Skip to main content

logicaffeine_kernel/
type_checker.rs

1//! Bidirectional type checker for the Calculus of Constructions.
2//!
3//! The type checker implements the typing rules of CoC:
4//!
5//! ```text
6//! ─────────────────────── (Sort)
7//!   Γ ⊢ Type n : Type (n+1)
8//!
9//!   Γ(x) = A
10//! ─────────────── (Var)
11//!   Γ ⊢ x : A
12//!
13//!   Γ ⊢ A : Type i    Γ, x:A ⊢ B : Type j
14//! ─────────────────────────────────────────── (Pi)
15//!          Γ ⊢ Π(x:A). B : Type max(i,j)
16//!
17//!   Γ ⊢ A : Type i    Γ, x:A ⊢ t : B
18//! ─────────────────────────────────────── (Lambda)
19//!      Γ ⊢ λ(x:A). t : Π(x:A). B
20//!
21//!   Γ ⊢ f : Π(x:A). B    Γ ⊢ a : A
22//! ───────────────────────────────────── (App)
23//!         Γ ⊢ f a : B[x := a]
24//! ```
25
26use crate::context::Context;
27use crate::error::{KernelError, KernelResult};
28use crate::reduction::normalize;
29use crate::term::{Literal, Term, Universe};
30
31/// Infer the type of a term in a context.
32///
33/// This is the main entry point for type checking. It implements bidirectional
34/// type inference for the Calculus of Constructions.
35///
36/// # Type Rules
37///
38/// - `Type n : Type (n+1)` - Universes form a hierarchy
39/// - `x : A` if `x : A` in context - Variable lookup
40/// - `Π(x:A). B : Type max(i,j)` if `A : Type i` and `B : Type j`
41/// - `λ(x:A). t : Π(x:A). B` if `t : B` in extended context
42/// - `f a : B[x := a]` if `f : Π(x:A). B` and `a : A`
43///
44/// # Errors
45///
46/// Returns [`KernelError`] variants for:
47/// - Unbound variables
48/// - Type mismatches in applications
49/// - Invalid match constructs
50/// - Termination check failures for fixpoints
51pub fn infer_type(ctx: &Context, term: &Term) -> KernelResult<Term> {
52    match term {
53        // Sort: Type n : Type (n+1)
54        Term::Sort(u) => Ok(Term::Sort(u.succ())),
55
56        // Var: lookup in local context
57        Term::Var(name) => ctx
58            .get(name)
59            .cloned()
60            .ok_or_else(|| KernelError::UnboundVariable(name.clone())),
61
62        // Global: lookup in global context (inductives and constructors)
63        Term::Global(name) => ctx
64            .get_global(name)
65            .cloned()
66            .ok_or_else(|| KernelError::UnboundVariable(name.clone())),
67
68        // Const: a universe-polymorphic global at explicit levels. Look up its stored
69        // universe parameters and type, then instantiate the parameters with `levels`.
70        Term::Const { name, levels } => {
71            let (params, ty, _body) = ctx
72                .get_universe_poly(name)
73                .ok_or_else(|| KernelError::UnboundVariable(name.clone()))?;
74            if params.len() != levels.len() {
75                return Err(KernelError::CertificationError(format!(
76                    "universe-polymorphic '{}' expects {} level argument(s), got {}",
77                    name,
78                    params.len(),
79                    levels.len()
80                )));
81            }
82            let subst: std::collections::HashMap<String, Universe> =
83                params.iter().cloned().zip(levels.iter().cloned()).collect();
84            Ok(crate::term::instantiate_universes(&ty.clone(), &subst))
85        }
86
87        // Pi: Π(x:A). B : Type max(sort(A), sort(B))
88        Term::Pi {
89            param,
90            param_type,
91            body_type,
92        } => {
93            // A must be a type
94            let a_sort = infer_sort(ctx, param_type)?;
95
96            // B must be a type in the extended context
97            let extended_ctx = ctx.extend(param, (**param_type).clone());
98            let b_sort = infer_sort(&extended_ctx, body_type)?;
99
100            // Product formation (CIC). `Prop` is **impredicative**: a Π whose
101            // codomain is a proposition is itself a proposition, no matter the
102            // domain's universe — so `∀x:Entity. P(x)` is a `Prop`, and FOL
103            // formulas built from it (And/Or/Ex over universals) stay in `Prop`
104            // where `And`/`Ex` require their arguments to live. `imax` is exactly
105            // this rule: `imax(a, Prop) = Prop`, `imax(a, non-Prop) = max(a, b)`,
106            // and it stays SYMBOLIC when the codomain level is a variable (whose
107            // Prop-ness is not yet known) — the case the old `_ => max` got wrong.
108            let pi_sort = a_sort.imax(&b_sort);
109            Ok(Term::Sort(pi_sort))
110        }
111
112        // Lambda: λ(x:A). t : Π(x:A). T where t : T
113        Term::Lambda {
114            param,
115            param_type,
116            body,
117        } => {
118            // Check param_type is well-formed (is a type)
119            let _ = infer_sort(ctx, param_type)?;
120
121            // Infer body type in extended context
122            let extended_ctx = ctx.extend(param, (**param_type).clone());
123            let body_type = infer_type(&extended_ctx, body)?;
124
125            // The lambda has a Pi type
126            Ok(Term::Pi {
127                param: param.clone(),
128                param_type: param_type.clone(),
129                body_type: Box::new(body_type),
130            })
131        }
132
133        // Let: `let x : A := v in b`. Check `A` is a type and `v : A`, then type
134        // the body with `x` bound TRANSPARENTLY — by zeta-substituting `v` for
135        // `x` (so `b`'s type sees `x ≡ v`, not an opaque hypothesis). This is
136        // exactly zeta-expansion, so it is trivially sound and identical in both
137        // kernels; the value is duplicated per occurrence during checking.
138        Term::Let { name, ty, value, body } => {
139            let _ = infer_sort(ctx, ty)?;
140            check_type(ctx, value, ty)?;
141            let unfolded = substitute(body, name, value);
142            infer_type(ctx, &unfolded)
143        }
144
145        // App: (f a) : B[x := a] where f : Π(x:A). B and a : A
146        Term::App(func, arg) => {
147            let func_type = infer_type(ctx, func)?;
148            // The function's type must be a Π, but it may be a redex that only REDUCES to
149            // one — e.g. a recursor result `P x` whose motive `P` is a λ. Normalize to
150            // expose the Π head before matching (the de Bruijn re-checker whnf's here too).
151            let func_type = match func_type {
152                Term::Pi { .. } => func_type,
153                other => normalize(ctx, &other),
154            };
155
156            match func_type {
157                Term::Pi {
158                    param,
159                    param_type,
160                    body_type,
161                } => {
162                    // Check argument has expected type
163                    check_type(ctx, arg, &param_type)?;
164
165                    // Substitute argument into body type
166                    Ok(substitute(&body_type, &param, arg))
167                }
168                _ => Err(KernelError::NotAFunction(format!("{}", func)))
169            }
170        }
171
172        // Match: pattern matching on inductive types
173        Term::Match {
174            discriminant,
175            motive,
176            cases,
177        } => {
178            // 1. Discriminant must have an inductive type. Normalize first, so a
179            // scrutinee whose type is a redex that reduces to an inductive (e.g. a
180            // motive application `(λb. …) true ⇝ False`) is still recognized.
181            let disc_type = normalize(ctx, &infer_type(ctx, discriminant)?);
182            let inductive_name = extract_inductive_name(ctx, &disc_type)
183                .ok_or_else(|| KernelError::NotAnInductive(format!("{}", disc_type)))?;
184
185            // Parameter/index split. `num_params` leading arguments of the inductive are
186            // uniform PARAMETERS fixed by the discriminant; any remaining arguments are
187            // INDICES that vary per constructor, over which the motive abstracts (`Eq`'s
188            // `P : Π(y:A). Eq A x y → Sort`). When there are no indices this is the
189            // ordinary eliminator, which takes the original path below byte-for-byte.
190            let disc_args = extract_type_args(&disc_type);
191            let num_params = ctx.inductive_num_params(&inductive_name).min(disc_args.len());
192            if disc_args.len() > num_params {
193                return infer_indexed_match(
194                    ctx,
195                    discriminant,
196                    motive,
197                    cases,
198                    &disc_type,
199                    &inductive_name,
200                    num_params,
201                    &disc_args,
202                );
203            }
204
205            // 2. Check motive is well-formed
206            // The motive can be either:
207            // - A function λ_:I. T (proper motive)
208            // - A raw type T (constant motive, wrapped automatically)
209            let motive_type = infer_type(ctx, motive)?;
210            let effective_motive = match &motive_type {
211                Term::Pi {
212                    param_type,
213                    body_type,
214                    ..
215                } => {
216                    // Motive is a function - check it takes the inductive type
217                    if !types_equal(param_type, &disc_type) {
218                        return Err(KernelError::InvalidMotive(format!(
219                            "motive parameter {} doesn't match discriminant type {}",
220                            param_type, disc_type
221                        )));
222                    }
223                    // body_type should be a Sort
224                    match infer_type(ctx, body_type) {
225                        Ok(Term::Sort(_)) => {}
226                        _ => {
227                            return Err(KernelError::InvalidMotive(format!(
228                                "motive body {} is not a type",
229                                body_type
230                            )));
231                        }
232                    }
233                    // Use motive as-is
234                    (**motive).clone()
235                }
236                Term::Sort(_) => {
237                    // Motive is a raw type - wrap in a constant function
238                    // λ_:disc_type. motive
239                    Term::Lambda {
240                        param: "_".to_string(),
241                        param_type: Box::new(disc_type.clone()),
242                        body: motive.clone(),
243                    }
244                }
245                _ => {
246                    return Err(KernelError::InvalidMotive(format!(
247                        "motive {} is not a function or type",
248                        motive
249                    )));
250                }
251            };
252
253            // 3. Check case count matches constructor count
254            let constructors = ctx.get_constructors(&inductive_name);
255            if cases.len() != constructors.len() {
256                return Err(KernelError::WrongNumberOfCases {
257                    expected: constructors.len(),
258                    found: cases.len(),
259                });
260            }
261
262            // 4. Check each case has the correct type
263            for (case, (ctor_name, ctor_type)) in cases.iter().zip(constructors.iter()) {
264                let expected_case_type = compute_case_type(&effective_motive, ctor_name, ctor_type, &disc_type);
265                check_type(ctx, case, &expected_case_type)?;
266            }
267
268            // 5. Return type is Motive(discriminant), beta-reduced
269            // Without beta reduction, (λ_:T. R) x returns the un-reduced form
270            // which causes type mismatches in nested matches.
271            let return_type = beta_reduce(&Term::App(Box::new(effective_motive), discriminant.clone()));
272
273            // 6. CIC large-elimination restriction. A `Prop` inductive may be eliminated into a larger
274            // sort (`Type`) ONLY if it is a subsingleton — zero constructors (so `ex falso` over `False`
275            // stays legal), or exactly one whose non-parameter arguments are all proofs (`And`, `eq`).
276            // Otherwise large elimination extracts computational content from a proof and breaks
277            // consistency: large-eliminating `Or` would let a *proof* pick a `Type`-level value.
278            if matches!(normalize(ctx, &infer_type(ctx, &disc_type)?), Term::Sort(Universe::Prop)) {
279                let large = !matches!(normalize(ctx, &infer_type(ctx, &return_type)?), Term::Sort(Universe::Prop));
280                if large && !is_subsingleton_prop(ctx, &inductive_name)? {
281                    return Err(KernelError::InvalidMotive(format!(
282                        "large elimination of proposition '{}' into a larger sort is not allowed: only \
283                         subsingleton propositions (empty, or one constructor with propositional arguments \
284                         — e.g. False, And, eq) may be eliminated into Type",
285                        inductive_name
286                    )));
287                }
288            }
289
290            Ok(return_type)
291        }
292
293        // Literal: infer type based on literal kind
294        Term::Lit(lit) => {
295            match lit {
296                Literal::Int(_) | Literal::BigInt(_) => Ok(Term::Global("Int".to_string())),
297                Literal::Nat(n) if *n < logicaffeine_base::BigInt::from_i64(0) => {
298                    Err(KernelError::CertificationError(
299                        "a `Nat` literal must be non-negative".to_string(),
300                    ))
301                }
302                Literal::Nat(_) => Ok(Term::Global("Nat".to_string())),
303                Literal::Float(_) => Ok(Term::Global("Float".to_string())),
304                Literal::Text(_) => Ok(Term::Global("Text".to_string())),
305                Literal::Duration(_) => Ok(Term::Global("Duration".to_string())),
306                Literal::Date(_) => Ok(Term::Global("Date".to_string())),
307                Literal::Moment(_) => Ok(Term::Global("Moment".to_string())),
308            }
309        }
310
311        // Hole: implicit argument, cannot infer type standalone
312        // Holes are handled specially in check_type
313        Term::Hole => Err(KernelError::CannotInferHole),
314
315        // Fix: fix f. body
316        // The type of (fix f. body) is the type of body when f is bound to that type.
317        // This is a fixpoint equation: T = type_of(body) where f : T.
318        //
319        // For typical fixpoints, body is a lambda: fix f. λx:A. e
320        // The type is Π(x:A). B where B is the type of e (with f : Π(x:A). B).
321        Term::Fix { name, body } => {
322            // For fix f. body, we need to handle the recursive reference to f.
323            // We structurally infer the type from lambda structure.
324            //
325            // This works because:
326            // 1. The body is typically nested lambdas with explicit parameter types
327            // 2. The return type is determined by the innermost expression's motive
328            // 3. Recursive calls have the same type as the fixpoint itself
329
330            // Extract the structural type from nested lambdas and motive
331            let structural_type = infer_fix_type_structurally(ctx, body)?;
332
333            // *** THE GUARDIAN: TERMINATION CHECK ***
334            // Verify that recursive calls decrease structurally.
335            // Without this check, we could "prove" False by looping forever.
336            crate::termination::check_termination(ctx, name, body)?;
337
338            // Sanity check: verify the body is well-formed with f bound
339            let extended = ctx.extend(name, structural_type.clone());
340            let _ = infer_type(&extended, body)?;
341
342            Ok(structural_type)
343        }
344
345        // MutualFix: a block of mutually-recursive definitions; this occurrence denotes
346        // the `index`-th one. Each definition's type is inferred structurally (like the
347        // single Fix), all names are put in scope, and the MUTUAL Giménez guard verifies
348        // termination before the bodies are sanity-checked with every sibling bound.
349        Term::MutualFix { defs, index } => {
350            if defs.is_empty() || *index >= defs.len() {
351                return Err(KernelError::CertificationError(
352                    "mutual fixpoint with an empty block or out-of-range index".to_string(),
353                ));
354            }
355
356            // Structural type of each definition (independent of the others' bodies).
357            let mut types = Vec::with_capacity(defs.len());
358            for (_, body) in defs {
359                types.push(infer_fix_type_structurally(ctx, body)?);
360            }
361
362            // *** THE GUARDIAN: MUTUAL TERMINATION CHECK ***
363            crate::termination::check_termination_mutual(ctx, defs)?;
364
365            // Sanity: every body is well-formed with ALL names bound to their structural
366            // types (a sibling call `rec_Odd n' o` sees `rec_Odd`'s type this way).
367            let mut extended = ctx.clone();
368            for ((name, _), ty) in defs.iter().zip(types.iter()) {
369                extended = extended.extend(name, ty.clone());
370            }
371            for (_, body) in defs {
372                let _ = infer_type(&extended, body)?;
373            }
374
375            Ok(types[*index].clone())
376        }
377    }
378}
379
380/// Infer the type of a fixpoint body structurally.
381///
382/// For `λx:A. body`, returns `Π(x:A). <type of body>`.
383/// This recursively handles nested lambdas.
384///
385/// The key insight is that for well-formed fixpoints, the body structure
386/// determines the type: parameters have explicit types, and the return type
387/// can be inferred from the innermost expression.
388fn infer_fix_type_structurally(ctx: &Context, term: &Term) -> KernelResult<Term> {
389    match term {
390        Term::Lambda {
391            param,
392            param_type,
393            body,
394        } => {
395            // Check param_type is well-formed
396            let _ = infer_sort(ctx, param_type)?;
397
398            // Extend context and recurse into body
399            let extended = ctx.extend(param, (**param_type).clone());
400            let body_type = infer_fix_type_structurally(&extended, body)?;
401
402            // Build Pi type
403            Ok(Term::Pi {
404                param: param.clone(),
405                param_type: param_type.clone(),
406                body_type: Box::new(body_type),
407            })
408        }
409        // For non-lambda bodies (the base case), we need to determine the return type.
410        // This is typically a Match whose motive determines it: the return type is the
411        // `motive` applied to the discriminant's INDEX arguments (for an indexed family) and
412        // then the discriminant itself, β-normalized. Computing the application — rather
413        // than just reading off the motive's body — is robust to the motive's binder names
414        // (which need not match the fixpoint's) and to indexed families. The discriminant is
415        // the fixpoint's structural binder, in scope, so its type is available without the
416        // not-yet-bound recursive name.
417        Term::Match { discriminant, motive, .. } => {
418            // A constant motive `return T` — one whose own type is a Sort — IS the result
419            // type and is not applied to the discriminant. Only a function motive `λx. P x`
420            // is applied to the discriminant's index arguments and then the discriminant
421            // itself. This mirrors the Term::Match inference rule, which wraps a Sort-typed
422            // motive as the constant `λ_:I. T` rather than applying it; without this guard a
423            // constant motive `Nat`/`List A` becomes the ill-formed `App(Nat, n)`.
424            if let Ok(mt) = infer_type(ctx, motive) {
425                if matches!(normalize(ctx, &mt), Term::Sort(_)) {
426                    return Ok(normalize(ctx, motive));
427                }
428            }
429            let mut applied = (**motive).clone();
430            if let Ok(dt) = infer_type(ctx, discriminant) {
431                let dt = normalize(ctx, &dt);
432                if let Some(ind) = extract_inductive_name(ctx, &dt) {
433                    let args = extract_type_args(&dt);
434                    let p = ctx.inductive_num_params(&ind).min(args.len());
435                    for idx in &args[p..] {
436                        applied = Term::App(Box::new(applied), Box::new(idx.clone()));
437                    }
438                }
439            }
440            applied = Term::App(Box::new(applied), discriminant.clone());
441            Ok(normalize(ctx, &applied))
442        }
443        // For other expressions, try normal inference
444        _ => infer_type(ctx, term),
445    }
446}
447
448/// Check that a term has the expected type (with subtyping/cumulativity).
449///
450/// Implements bidirectional type checking: when checking a Lambda against a Pi,
451/// we can use the Pi's parameter type instead of the Lambda's (which may be a
452/// placeholder from match case parsing).
453fn check_type(ctx: &Context, term: &Term, expected: &Term) -> KernelResult<()> {
454    // Hole as term: accept if expected is a Sort (Hole stands for a type)
455    // This allows `Eq Hole X Y` where Eq expects Type as first arg
456    if matches!(term, Term::Hole) {
457        if matches!(expected, Term::Sort(_)) {
458            return Ok(());
459        }
460        return Err(KernelError::TypeMismatch {
461            expected: format!("{}", expected),
462            found: "_".to_string(),
463        });
464    }
465
466    // Hole as expected type: accept any well-typed term
467    // This allows checking args against Hole in `(Eq Hole) X Y` intermediates
468    if matches!(expected, Term::Hole) {
469        let _ = infer_type(ctx, term)?; // Just verify term is well-typed
470        return Ok(());
471    }
472
473    // Special case: Lambda with placeholder type checked against Pi
474    // This handles match cases where binder types come from the expected type
475    if let Term::Lambda {
476        param,
477        param_type,
478        body,
479    } = term
480    {
481        // Check if param_type is a placeholder ("_")
482        if let Term::Global(name) = param_type.as_ref() {
483            if name == "_" {
484                // Bidirectional mode: get param type from expected
485                if let Term::Pi {
486                    param_type: expected_param_type,
487                    body_type: expected_body_type,
488                    param: expected_param,
489                } = expected
490                {
491                    // Check body in extended context using expected param type
492                    let extended_ctx = ctx.extend(param, (**expected_param_type).clone());
493                    // Substitute in expected_body_type if param names differ
494                    let body_expected = if param != expected_param {
495                        substitute(expected_body_type, expected_param, &Term::Var(param.clone()))
496                    } else {
497                        (**expected_body_type).clone()
498                    };
499                    return check_type(&extended_ctx, body, &body_expected);
500                }
501            }
502        }
503    }
504
505    let inferred = infer_type(ctx, term)?;
506    if is_subtype(ctx, &inferred, expected) {
507        Ok(())
508    } else {
509        Err(KernelError::TypeMismatch {
510            expected: format!("{}", expected),
511            found: format!("{}", inferred),
512        })
513    }
514}
515
516/// Infer the sort (universe) of a type.
517///
518/// A term is a type if its type is a Sort.
519fn infer_sort(ctx: &Context, term: &Term) -> KernelResult<Universe> {
520    let ty = infer_type(ctx, term)?;
521    match ty {
522        Term::Sort(u) => Ok(u),
523        _ => Err(KernelError::NotAType(format!("{}", term))),
524    }
525}
526
527/// The RESULT sort of an inductive's arity — peel its leading `Π`s to the final `Sort`.
528fn result_sort_universe(t: &Term) -> Option<Universe> {
529    let mut cur = t;
530    while let Term::Pi { body_type, .. } = cur {
531        cur = body_type;
532    }
533    match cur {
534        Term::Sort(u) => Some(u.clone()),
535        _ => None,
536    }
537}
538
539/// Check the CIC UNIVERSE CONSTRAINT of an inductive constructor: every VALUE argument's
540/// sort must be `≤` the inductive's result sort — so a `Type 0` inductive cannot store a
541/// `Type 0`-typed field (which lives in `Type 1`), the universe inconsistency that opens
542/// Girard/Hurkens paradoxes. A `Prop` inductive is exempt (impredicative `Prop` admits
543/// arguments of any sort), exactly as in Coq/Lean. The inductive (and any mutual siblings
544/// a recursive field references) must already be registered in `ctx`.
545pub fn check_constructor_universes(
546    ctx: &Context,
547    ind: &str,
548    ctor: &str,
549    ty: &Term,
550) -> KernelResult<()> {
551    let ind_ty = match ctx.get_global(ind) {
552        Some(t) => t.clone(),
553        None => return Ok(()),
554    };
555    let target = match result_sort_universe(&ind_ty) {
556        // Impredicative Prop: no constraint on argument universes.
557        Some(Universe::Prop) => return Ok(()),
558        Some(u) => u,
559        None => return Ok(()),
560    };
561    let num_params = ctx.inductive_num_params(ind);
562    // Walk the constructor telescope; the leading `num_params` are the inductive's uniform
563    // parameters, the rest are stored VALUE fields subject to the constraint.
564    let mut ext = ctx.clone();
565    let mut cur = ty;
566    let mut i = 0usize;
567    while let Term::Pi { param, param_type, body_type } = cur {
568        if i >= num_params {
569            let s = infer_sort(&ext, param_type)?;
570            if !s.is_subtype_of(&target) {
571                return Err(KernelError::CertificationError(format!(
572                    "universe inconsistency: constructor '{ctor}' stores an argument in sort \
573                     {s}, which exceeds the sort {target} of its inductive '{ind}'"
574                )));
575            }
576        }
577        ext = ext.extend(param, (**param_type).clone());
578        cur = body_type;
579        i += 1;
580    }
581    Ok(())
582}
583
584/// Beta-reduce a term (single step, at the head).
585///
586/// (λx.body) arg → body[x := arg]
587fn beta_reduce(term: &Term) -> Term {
588    match term {
589        Term::App(func, arg) => {
590            match func.as_ref() {
591                Term::Lambda { param, body, .. } => {
592                    // Beta reduction: (λx.body) arg → body[x := arg]
593                    substitute(body, param, arg)
594                }
595                _ => term.clone(),
596            }
597        }
598        _ => term.clone(),
599    }
600}
601
602/// Type an INDEXED match: the discriminant's inductive has `num_params` uniform
603/// parameters and one or more trailing INDICES, and the `motive` abstracts over those
604/// indices plus the scrutinee — `P : Π(indices…). Π(z : I params indices). Sort`.
605///
606/// The return type is `motive` applied to the discriminant's own index arguments and then
607/// the discriminant itself; each constructor's case is checked against `motive` applied to
608/// THAT constructor's result indices and the constructor value. Soundness rides on the
609/// final `infer_type(return_type)` being a `Sort`: an ill-shaped motive makes those
610/// applications fail to type-check, so nothing unsound slips through.
611#[allow(clippy::too_many_arguments)]
612fn infer_indexed_match(
613    ctx: &Context,
614    discriminant: &Term,
615    motive: &Term,
616    cases: &[Term],
617    disc_type: &Term,
618    inductive_name: &str,
619    num_params: usize,
620    disc_args: &[Term],
621) -> KernelResult<Term> {
622    // The discriminant's own parameter args (fixed) and index args (what the motive is
623    // instantiated at for the *result* type).
624    let disc_params = &disc_args[0..num_params];
625    let disc_indices = &disc_args[num_params..];
626
627    // The motive must at least type-check; its shape is enforced structurally by the case
628    // and return-type checks below.
629    let _ = infer_type(ctx, motive)?;
630
631    // Coverage: exactly one case per constructor, in registration order.
632    let constructors = ctx.get_constructors(inductive_name);
633    if cases.len() != constructors.len() {
634        return Err(KernelError::WrongNumberOfCases {
635            expected: constructors.len(),
636            found: cases.len(),
637        });
638    }
639
640    // Each case against its indexed constructor type.
641    for (case, (ctor_name, ctor_type)) in cases.iter().zip(constructors.iter()) {
642        let expected = compute_indexed_case_type(motive, ctor_name, ctor_type, num_params, disc_params);
643        check_type(ctx, case, &expected)?;
644    }
645
646    // Return type: `motive disc_index₁ … disc_indexₖ discriminant`, normalized.
647    let mut ret = motive.clone();
648    for idx in disc_indices {
649        ret = Term::App(Box::new(ret), Box::new(idx.clone()));
650    }
651    ret = Term::App(Box::new(ret), Box::new(discriminant.clone()));
652    let ret = normalize(ctx, &ret);
653
654    // The result must be a type — this is what certifies the motive is a well-formed
655    // family into a sort (a non-family motive would not infer to a `Sort` here).
656    match normalize(ctx, &infer_type(ctx, &ret)?) {
657        Term::Sort(_) => {}
658        other => {
659            return Err(KernelError::InvalidMotive(format!(
660                "indexed match on '{}' has non-type result {} — the motive is not a family into a sort",
661                inductive_name, other
662            )));
663        }
664    }
665
666    // CIC large-elimination restriction (identical rule to the non-indexed path).
667    if matches!(normalize(ctx, &infer_type(ctx, disc_type)?), Term::Sort(Universe::Prop)) {
668        let large = !matches!(normalize(ctx, &infer_type(ctx, &ret)?), Term::Sort(Universe::Prop));
669        if large && !is_subsingleton_prop(ctx, inductive_name)? {
670            return Err(KernelError::InvalidMotive(format!(
671                "large elimination of proposition '{}' into a larger sort is not allowed",
672                inductive_name
673            )));
674        }
675    }
676
677    Ok(ret)
678}
679
680/// The expected type of one constructor's case in an INDEXED match: the constructor's
681/// leading `num_params` parameters are instantiated by the discriminant's `disc_params`;
682/// its remaining value arguments become the case's `Π` binders; and the codomain is the
683/// `motive` applied to the constructor's RESULT indices (as they appear in its declared
684/// return type) and then the constructor value itself.
685fn compute_indexed_case_type(
686    motive: &Term,
687    ctor_name: &str,
688    ctor_type: &Term,
689    num_params: usize,
690    disc_params: &[Term],
691) -> Term {
692    // Peel the constructor's full Π telescope; the residual is its result `I params… idx…`.
693    let mut all_params: Vec<(String, Term)> = Vec::new();
694    let mut current = ctor_type;
695    while let Term::Pi { param, param_type, body_type } = current {
696        all_params.push((param.clone(), (**param_type).clone()));
697        current = body_type;
698    }
699    let result_args = extract_type_args(current);
700
701    let split = num_params.min(all_params.len());
702    let param_binders = &all_params[0..split];
703    // Fresh names for the value parameters (those past the inductive's parameters).
704    let value_named: Vec<(String, String, Term)> = all_params[split..]
705        .iter()
706        .enumerate()
707        .map(|(i, (orig, ty))| (orig.clone(), format!("__arg{}", i), ty.clone()))
708        .collect();
709
710    // Rewrite a term from the constructor's scope into the case's scope: parameter names →
711    // the discriminant's parameter arguments, and each value parameter → its fresh name.
712    // `upto` bounds which value parameters are already in scope (for dependent arg types).
713    let rewrite = |t: &Term, upto: usize| -> Term {
714        let mut out = t.clone();
715        for (i, (name, _)) in param_binders.iter().enumerate() {
716            if name != "_" {
717                out = substitute(&out, name, &disc_params[i]);
718            }
719        }
720        for (orig, fresh, _) in value_named.iter().take(upto) {
721            if orig != "_" {
722                out = substitute(&out, orig, &Term::Var(fresh.clone()));
723            }
724        }
725        out
726    };
727
728    // The constructor's result index expressions (its result args past the parameters),
729    // rewritten into the case's scope.
730    let index_exprs: Vec<Term> = result_args
731        .iter()
732        .skip(split)
733        .map(|e| beta_reduce(&rewrite(e, value_named.len())))
734        .collect();
735
736    // `C disc_params… value_vars…`.
737    let mut ctor_applied = Term::Global(ctor_name.to_string());
738    for pa in disc_params {
739        ctor_applied = Term::App(Box::new(ctor_applied), Box::new(pa.clone()));
740    }
741    for (_, fresh, _) in &value_named {
742        ctor_applied = Term::App(Box::new(ctor_applied), Box::new(Term::Var(fresh.clone())));
743    }
744
745    // `motive index_exprs… ctor_applied`, beta-reduced.
746    let mut body = motive.clone();
747    for e in &index_exprs {
748        body = Term::App(Box::new(body), Box::new(e.clone()));
749    }
750    body = Term::App(Box::new(body), Box::new(ctor_applied));
751    let mut case_type = beta_reduce(&body);
752
753    // Re-wrap the value parameters as `Π`, each type closed into the case's scope.
754    for k in (0..value_named.len()).rev() {
755        let (_, fresh, ty_k) = &value_named[k];
756        let pty = beta_reduce(&rewrite(ty_k, k));
757        case_type = Term::Pi {
758            param: fresh.clone(),
759            param_type: Box::new(pty),
760            body_type: Box::new(case_type),
761        };
762    }
763
764    case_type
765}
766
767/// Compute the expected type for a match case.
768///
769/// For a constructor C : A₁ → A₂ → ... → I,
770/// the case type is: Πa₁:A₁. Πa₂:A₂. ... P(C a₁ a₂ ...)
771///
772/// For a zero-argument constructor like Zero : Nat,
773/// the case type is just P(Zero).
774///
775/// For polymorphic constructors like Nil : Π(A:Type). List A,
776/// when matching on `xs : List A`, we skip the type parameter
777/// and use the instantiated type argument instead.
778fn compute_case_type(motive: &Term, ctor_name: &str, ctor_type: &Term, disc_type: &Term) -> Term {
779    // Extract type arguments from discriminant type
780    // e.g., List A → [A], List → []
781    let type_args = extract_type_args(disc_type);
782    let num_type_args = type_args.len();
783
784    // Collect parameters from constructor type
785    let mut all_params: Vec<(String, Term)> = Vec::new();
786    let mut current = ctor_type;
787
788    while let Term::Pi {
789        param,
790        param_type,
791        body_type,
792    } = current
793    {
794        all_params.push((param.clone(), (**param_type).clone()));
795        current = body_type;
796    }
797
798    // Split into type parameters (fixed by the discriminant) and value
799    // parameters (bound by the case). The type parameters are the first
800    // `num_type_args` constructor arguments.
801    let type_params: Vec<(String, Term)> = all_params
802        .iter()
803        .take(num_type_args)
804        .map(|(n, t)| (n.clone(), t.clone()))
805        .collect();
806    // For each value parameter keep (original name, fresh name, type). The
807    // original name matters: a *dependent* constructor (e.g. `Ex`'s
808    // `witness : … Π(x:A). P x → Ex A P`) has later argument types that mention
809    // earlier value parameters, so those references must be rewritten to the
810    // fresh names too — not just the type parameters.
811    let value_named: Vec<(String, String, Term)> = all_params
812        .into_iter()
813        .skip(num_type_args)
814        .enumerate()
815        .map(|(i, (orig, ty))| (orig, format!("__arg{}", i), ty))
816        .collect();
817
818    // Build `C type_args… value_args…` with the fresh value-arg names.
819    let mut ctor_applied = Term::Global(ctor_name.to_string());
820    for type_arg in &type_args {
821        ctor_applied = Term::App(Box::new(ctor_applied), Box::new(type_arg.clone()));
822    }
823    for (_, new_name, _) in &value_named {
824        ctor_applied = Term::App(Box::new(ctor_applied), Box::new(Term::Var(new_name.clone())));
825    }
826
827    // `motive (C …)`, beta-reduced.
828    let result_type = beta_reduce(&Term::App(Box::new(motive.clone()), Box::new(ctor_applied)));
829
830    // Wrap in Π over the value parameters (reverse order for correct nesting).
831    // Each parameter's type is closed by substituting the type parameters and
832    // every *earlier* value parameter (original → fresh), then beta-reduced so a
833    // dependent type like `P x` collapses to its applied form (e.g. `evil x`).
834    let mut case_type = result_type;
835    for k in (0..value_named.len()).rev() {
836        let (_, new_name, ty_k) = &value_named[k];
837        let mut pty = ty_k.clone();
838        for ((tp_name, _), type_arg) in type_params.iter().zip(type_args.iter()) {
839            pty = substitute(&pty, tp_name, type_arg);
840        }
841        for (orig_j, new_j, _) in value_named.iter().take(k) {
842            pty = substitute(&pty, orig_j, &Term::Var(new_j.clone()));
843        }
844        let pty = beta_reduce(&pty);
845        case_type = Term::Pi {
846            param: new_name.clone(),
847            param_type: Box::new(pty),
848            body_type: Box::new(case_type),
849        };
850    }
851
852    case_type
853}
854
855/// Extract type arguments from a type application.
856///
857/// - `List A` → `[A]`
858/// - `Either A B` → `[A, B]`
859/// - `Nat` → `[]`
860fn extract_type_args(ty: &Term) -> Vec<Term> {
861    let mut args = Vec::new();
862    let mut current = ty;
863
864    while let Term::App(func, arg) = current {
865        args.push((**arg).clone());
866        current = func;
867    }
868
869    args.reverse();
870    args
871}
872
873/// Substitute a term for a variable: `body[var := replacement]`.
874///
875/// Performs capture-avoiding substitution. Variables bound by lambda,
876/// pi, or fix that shadow `var` are not substituted into.
877///
878/// # Capture Avoidance
879///
880/// Given `substitute(λx. y, "y", x)`, the result is `λx. x` (not `λx. x`
881/// with the inner x captured). This implementation relies on unique
882/// variable names from parsing.
883///
884/// # Term Forms
885///
886/// - `Sort`, `Lit`, `Hole`, `Global` - Unchanged (no variables)
887/// - `Var(name)` - Replaced if `name == var`, unchanged otherwise
888/// - `Pi`, `Lambda`, `Fix` - Substitute in components, respecting shadowing
889/// - `App`, `Match` - Substitute recursively in all subterms
890pub fn substitute(body: &Term, var: &str, replacement: &Term) -> Term {
891    // Fast path: if `var` does not occur free in `body`, the substitution is the
892    // identity. Crucially this lets us skip `free_vars(replacement)` — and the
893    // `replacement` is, in `App`/`Match` type inference, the WHOLE argument/discriminant
894    // proof term. A propositional implication's codomain never mentions the bound proof
895    // variable (non-dependent), so this is the common case and turns quadratic checking
896    // (walk the giant argument at every application) into linear.
897    if !occurs_free(body, var) {
898        return body.clone();
899    }
900    // Compute the free variables of the replacement once; a binder in `body`
901    // that captures any of them must be alpha-renamed before we descend.
902    let replacement_fvs = free_vars(replacement);
903    substitute_avoiding(body, var, replacement, &replacement_fvs)
904}
905
906/// Whether `var` occurs free in `term` (binder-aware, short-circuiting).
907fn occurs_free(term: &Term, var: &str) -> bool {
908    match term {
909        Term::Var(name) => name == var,
910        Term::Sort(_) | Term::Lit(_) | Term::Hole | Term::Global(_) | Term::Const { .. } => false,
911        Term::App(func, arg) => occurs_free(func, var) || occurs_free(arg, var),
912        Term::Pi { param, param_type, body_type } => {
913            occurs_free(param_type, var) || (param != var && occurs_free(body_type, var))
914        }
915        Term::Lambda { param, param_type, body } => {
916            occurs_free(param_type, var) || (param != var && occurs_free(body, var))
917        }
918        Term::Fix { name, body } => name != var && occurs_free(body, var),
919        Term::MutualFix { defs, .. } => {
920            // `var` is free only if it is not one of the (all-binding) def names AND
921            // occurs free in some body.
922            !defs.iter().any(|(n, _)| n == var) && defs.iter().any(|(_, b)| occurs_free(b, var))
923        }
924        Term::Let { name, ty, value, body } => {
925            occurs_free(ty, var)
926                || occurs_free(value, var)
927                || (name != var && occurs_free(body, var))
928        }
929        Term::Match { discriminant, motive, cases } => {
930            occurs_free(discriminant, var)
931                || occurs_free(motive, var)
932                || cases.iter().any(|c| occurs_free(c, var))
933        }
934    }
935}
936
937/// Collect the free variables of a term (named representation).
938fn free_vars(term: &Term) -> std::collections::HashSet<String> {
939    fn go(term: &Term, bound: &mut Vec<String>, acc: &mut std::collections::HashSet<String>) {
940        match term {
941            Term::Var(name) => {
942                if !bound.iter().any(|b| b == name) {
943                    acc.insert(name.clone());
944                }
945            }
946            Term::Sort(_) | Term::Lit(_) | Term::Hole | Term::Global(_) | Term::Const { .. } => {}
947            Term::App(func, arg) => {
948                go(func, bound, acc);
949                go(arg, bound, acc);
950            }
951            Term::Pi { param, param_type, body_type } => {
952                go(param_type, bound, acc);
953                bound.push(param.clone());
954                go(body_type, bound, acc);
955                bound.pop();
956            }
957            Term::Lambda { param, param_type, body } => {
958                go(param_type, bound, acc);
959                bound.push(param.clone());
960                go(body, bound, acc);
961                bound.pop();
962            }
963            Term::Fix { name, body } => {
964                bound.push(name.clone());
965                go(body, bound, acc);
966                bound.pop();
967            }
968            Term::MutualFix { defs, .. } => {
969                for (n, _) in defs {
970                    bound.push(n.clone());
971                }
972                for (_, b) in defs {
973                    go(b, bound, acc);
974                }
975                for _ in defs {
976                    bound.pop();
977                }
978            }
979            Term::Let { name, ty, value, body } => {
980                go(ty, bound, acc);
981                go(value, bound, acc);
982                bound.push(name.clone());
983                go(body, bound, acc);
984                bound.pop();
985            }
986            Term::Match { discriminant, motive, cases } => {
987                go(discriminant, bound, acc);
988                go(motive, bound, acc);
989                for c in cases {
990                    go(c, bound, acc);
991                }
992            }
993        }
994    }
995    let mut acc = std::collections::HashSet::new();
996    let mut bound = Vec::new();
997    go(term, &mut bound, &mut acc);
998    acc
999}
1000
1001/// Collect every variable name appearing in a term (bound or free).
1002fn all_var_names(term: &Term, acc: &mut std::collections::HashSet<String>) {
1003    match term {
1004        Term::Var(name) => {
1005            acc.insert(name.clone());
1006        }
1007        Term::Sort(_) | Term::Lit(_) | Term::Hole | Term::Global(_) | Term::Const { .. } => {}
1008        Term::App(func, arg) => {
1009            all_var_names(func, acc);
1010            all_var_names(arg, acc);
1011        }
1012        Term::Pi { param, param_type, body_type } => {
1013            acc.insert(param.clone());
1014            all_var_names(param_type, acc);
1015            all_var_names(body_type, acc);
1016        }
1017        Term::Lambda { param, param_type, body } => {
1018            acc.insert(param.clone());
1019            all_var_names(param_type, acc);
1020            all_var_names(body, acc);
1021        }
1022        Term::Fix { name, body } => {
1023            acc.insert(name.clone());
1024            all_var_names(body, acc);
1025        }
1026        Term::MutualFix { defs, .. } => {
1027            for (n, b) in defs {
1028                acc.insert(n.clone());
1029                all_var_names(b, acc);
1030            }
1031        }
1032        Term::Let { name, ty, value, body } => {
1033            acc.insert(name.clone());
1034            all_var_names(ty, acc);
1035            all_var_names(value, acc);
1036            all_var_names(body, acc);
1037        }
1038        Term::Match { discriminant, motive, cases } => {
1039            all_var_names(discriminant, acc);
1040            all_var_names(motive, acc);
1041            for c in cases {
1042                all_var_names(c, acc);
1043            }
1044        }
1045    }
1046}
1047
1048/// Pick a binder name derived from `base` that collides with nothing in `avoid`.
1049fn fresh_name(base: &str, avoid: &std::collections::HashSet<String>) -> String {
1050    let mut candidate = format!("{}'", base);
1051    let mut counter: u32 = 0;
1052    while avoid.contains(&candidate) {
1053        counter += 1;
1054        candidate = format!("{}'{}", base, counter);
1055    }
1056    candidate
1057}
1058
1059/// Choose a fresh binder name for `param`, avoiding the replacement's free vars,
1060/// every name occurring in `body`, and the variable being substituted.
1061fn freshen(
1062    param: &str,
1063    body: &Term,
1064    replacement_fvs: &std::collections::HashSet<String>,
1065    var: &str,
1066) -> String {
1067    let mut avoid = replacement_fvs.clone();
1068    all_var_names(body, &mut avoid);
1069    avoid.insert(var.to_string());
1070    fresh_name(param, &avoid)
1071}
1072
1073/// Rename free occurrences of `from` to `to` in `term`. `to` must be globally
1074/// fresh in `term`, so this rename cannot itself capture.
1075fn rename_var(term: &Term, from: &str, to: &str) -> Term {
1076    let repl = Term::Var(to.to_string());
1077    let mut fvs = std::collections::HashSet::new();
1078    fvs.insert(to.to_string());
1079    substitute_avoiding(term, from, &repl, &fvs)
1080}
1081
1082/// Capture-avoiding substitution `body[var := replacement]`, where
1083/// `replacement_fvs` is the precomputed free-variable set of `replacement`.
1084///
1085/// When a binder (`Pi`/`Lambda`/`Fix`) would capture a free variable of
1086/// `replacement`, the binder is alpha-renamed to a fresh name before the
1087/// substitution descends into its body.
1088fn substitute_avoiding(
1089    body: &Term,
1090    var: &str,
1091    replacement: &Term,
1092    replacement_fvs: &std::collections::HashSet<String>,
1093) -> Term {
1094    match body {
1095        Term::Sort(u) => Term::Sort(u.clone()),
1096
1097        // A universe-polymorphic reference has no term variables to substitute.
1098        Term::Const { .. } => body.clone(),
1099
1100        // Literals are never substituted
1101        Term::Lit(lit) => Term::Lit(lit.clone()),
1102
1103        // Holes are never substituted (they're implicit type placeholders)
1104        Term::Hole => Term::Hole,
1105
1106        Term::Var(name) if name == var => replacement.clone(),
1107        Term::Var(name) => Term::Var(name.clone()),
1108
1109        // Globals are never substituted (they're not bound variables)
1110        Term::Global(name) => Term::Global(name.clone()),
1111
1112        Term::Pi {
1113            param,
1114            param_type,
1115            body_type,
1116        } => {
1117            let new_param_type = substitute_avoiding(param_type, var, replacement, replacement_fvs);
1118            if param == var {
1119                // The parameter shadows `var`: do not substitute in the body.
1120                Term::Pi {
1121                    param: param.clone(),
1122                    param_type: Box::new(new_param_type),
1123                    body_type: (*body_type).clone(),
1124                }
1125            } else if replacement_fvs.contains(param) {
1126                // Capture-avoidance: rename the binder away from the free vars
1127                // of `replacement` before substituting into the body.
1128                let fresh = freshen(param, body_type, replacement_fvs, var);
1129                let renamed = rename_var(body_type, param, &fresh);
1130                Term::Pi {
1131                    param: fresh,
1132                    param_type: Box::new(new_param_type),
1133                    body_type: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
1134                }
1135            } else {
1136                Term::Pi {
1137                    param: param.clone(),
1138                    param_type: Box::new(new_param_type),
1139                    body_type: Box::new(substitute_avoiding(body_type, var, replacement, replacement_fvs)),
1140                }
1141            }
1142        }
1143
1144        Term::Lambda {
1145            param,
1146            param_type,
1147            body,
1148        } => {
1149            let new_param_type = substitute_avoiding(param_type, var, replacement, replacement_fvs);
1150            if param == var {
1151                // The parameter shadows `var`: do not substitute in the body.
1152                Term::Lambda {
1153                    param: param.clone(),
1154                    param_type: Box::new(new_param_type),
1155                    body: (*body).clone(),
1156                }
1157            } else if replacement_fvs.contains(param) {
1158                // Capture-avoidance: rename the binder away from the free vars
1159                // of `replacement` before substituting into the body.
1160                let fresh = freshen(param, body, replacement_fvs, var);
1161                let renamed = rename_var(body, param, &fresh);
1162                Term::Lambda {
1163                    param: fresh,
1164                    param_type: Box::new(new_param_type),
1165                    body: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
1166                }
1167            } else {
1168                Term::Lambda {
1169                    param: param.clone(),
1170                    param_type: Box::new(new_param_type),
1171                    body: Box::new(substitute_avoiding(body, var, replacement, replacement_fvs)),
1172                }
1173            }
1174        }
1175
1176        Term::App(func, arg) => Term::App(
1177            Box::new(substitute_avoiding(func, var, replacement, replacement_fvs)),
1178            Box::new(substitute_avoiding(arg, var, replacement, replacement_fvs)),
1179        ),
1180
1181        Term::Match {
1182            discriminant,
1183            motive,
1184            cases,
1185        } => Term::Match {
1186            discriminant: Box::new(substitute_avoiding(discriminant, var, replacement, replacement_fvs)),
1187            motive: Box::new(substitute_avoiding(motive, var, replacement, replacement_fvs)),
1188            cases: cases
1189                .iter()
1190                .map(|c| substitute_avoiding(c, var, replacement, replacement_fvs))
1191                .collect(),
1192        },
1193
1194        Term::Fix { name, body } => {
1195            if name == var {
1196                // The fixpoint name shadows `var`: do not substitute in the body.
1197                Term::Fix {
1198                    name: name.clone(),
1199                    body: body.clone(),
1200                }
1201            } else if replacement_fvs.contains(name) {
1202                // Capture-avoidance: rename the fixpoint binder.
1203                let fresh = freshen(name, body, replacement_fvs, var);
1204                let renamed = rename_var(body, name, &fresh);
1205                Term::Fix {
1206                    name: fresh,
1207                    body: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
1208                }
1209            } else {
1210                Term::Fix {
1211                    name: name.clone(),
1212                    body: Box::new(substitute_avoiding(body, var, replacement, replacement_fvs)),
1213                }
1214            }
1215        }
1216
1217        Term::MutualFix { defs, index } => {
1218            // Every def name binds in EVERY body. If `var` is one of them it is shadowed
1219            // throughout — leave the whole block untouched.
1220            if defs.iter().any(|(n, _)| n == var) {
1221                return body.clone();
1222            }
1223            // Capture-avoidance: any def name that is a free var of `replacement` must be
1224            // α-renamed CONSISTENTLY across all bodies (it is one mutual binder shared by
1225            // all) before the substitution descends.
1226            let mut names: Vec<String> = defs.iter().map(|(n, _)| n.clone()).collect();
1227            let mut bodies: Vec<Term> = defs.iter().map(|(_, b)| b.clone()).collect();
1228            for i in 0..names.len() {
1229                if replacement_fvs.contains(&names[i]) {
1230                    let mut avoid = replacement_fvs.clone();
1231                    for b in &bodies {
1232                        all_var_names(b, &mut avoid);
1233                    }
1234                    for n in &names {
1235                        avoid.insert(n.clone());
1236                    }
1237                    avoid.insert(var.to_string());
1238                    let fresh = fresh_name(&names[i], &avoid);
1239                    for b in bodies.iter_mut() {
1240                        *b = rename_var(b, &names[i], &fresh);
1241                    }
1242                    names[i] = fresh;
1243                }
1244            }
1245            let new_defs = names
1246                .into_iter()
1247                .zip(bodies.iter())
1248                .map(|(n, b)| (n, substitute_avoiding(b, var, replacement, replacement_fvs)))
1249                .collect();
1250            Term::MutualFix { defs: new_defs, index: *index }
1251        }
1252
1253        Term::Let { name, ty, value, body } => {
1254            // `ty` and `value` are outside the `name` binder — always substitute.
1255            let new_ty = substitute_avoiding(ty, var, replacement, replacement_fvs);
1256            let new_value = substitute_avoiding(value, var, replacement, replacement_fvs);
1257            if name == var {
1258                // The let-binder shadows `var`: leave the body untouched.
1259                Term::Let {
1260                    name: name.clone(),
1261                    ty: Box::new(new_ty),
1262                    value: Box::new(new_value),
1263                    body: body.clone(),
1264                }
1265            } else if replacement_fvs.contains(name) {
1266                // Capture-avoidance: rename the let-binder away from the
1267                // replacement's free vars before substituting into the body.
1268                let fresh = freshen(name, body, replacement_fvs, var);
1269                let renamed = rename_var(body, name, &fresh);
1270                Term::Let {
1271                    name: fresh,
1272                    ty: Box::new(new_ty),
1273                    value: Box::new(new_value),
1274                    body: Box::new(substitute_avoiding(&renamed, var, replacement, replacement_fvs)),
1275                }
1276            } else {
1277                Term::Let {
1278                    name: name.clone(),
1279                    ty: Box::new(new_ty),
1280                    value: Box::new(new_value),
1281                    body: Box::new(substitute_avoiding(body, var, replacement, replacement_fvs)),
1282                }
1283            }
1284        }
1285    }
1286}
1287
1288/// Check if type `a` is a subtype of type `b` (cumulativity).
1289///
1290/// Implements the subtyping relation for the Calculus of Constructions
1291/// with cumulative universes.
1292///
1293/// # Subtyping Rules
1294///
1295/// - **Universe cumulativity**: `Type i <= Type j` if `i <= j`
1296/// - **Pi contravariance**: `Π(x:A). B <= Π(x:A'). B'` if `A' <= A` and `B <= B'`
1297/// - **Structural equality**: Other terms are compared after normalization
1298///
1299/// # Normalization
1300///
1301/// Both types are normalized before comparison, ensuring that definitionally
1302/// equal types are recognized as subtypes.
1303///
1304/// # Cumulativity Examples
1305///
1306/// - `Type 0 <= Type 1` (lower universe is subtype of higher)
1307/// - `Nat -> Type 0 <= Nat -> Type 1` (covariant in return type)
1308/// - `Type 1 -> Nat <= Type 0 -> Nat` (contravariant in parameter type)
1309pub fn is_subtype(ctx: &Context, a: &Term, b: &Term) -> bool {
1310    // Fast path: already structurally (definitionally) equal — the overwhelming case
1311    // in propositional/FOL proofs, where types are atoms and ∧/∨/¬/→ in normal form.
1312    // Skipping the two `normalize` walks here is the difference between linear and
1313    // pathological checking on a large certified grid proof.
1314    if types_equal(a, b) {
1315        return true;
1316    }
1317    // Otherwise normalize both terms before comparison — this ensures that e.g.
1318    // `ReachesOne (collatzStep 2)` equals `ReachesOne 1`.
1319    let a_norm = normalize(ctx, a);
1320    let b_norm = normalize(ctx, b);
1321
1322    is_subtype_normalized(ctx, &a_norm, &b_norm)
1323}
1324
1325/// Check subtyping on already-normalized terms. Cumulativity lives ONLY here: universes
1326/// follow `Prop ≤ Type i ≤ Type j`, and a `Π` is contravariant in its domain and covariant
1327/// in its codomain. Every other position is INVARIANT and delegates to [`def_eq_normalized`]
1328/// (definitional equality: reduction + η + proof irrelevance) — using cumulative subtyping
1329/// in an invariant position (e.g. a function argument) would be unsound.
1330fn is_subtype_normalized(ctx: &Context, a: &Term, b: &Term) -> bool {
1331    match (a, b) {
1332        // Universe subtyping
1333        (Term::Sort(u1), Term::Sort(u2)) => u1.is_subtype_of(u2),
1334
1335        // Pi subtyping (contravariant in param, covariant in body)
1336        (
1337            Term::Pi {
1338                param: p1,
1339                param_type: t1,
1340                body_type: b1,
1341            },
1342            Term::Pi {
1343                param: p2,
1344                param_type: t2,
1345                body_type: b2,
1346            },
1347        ) => {
1348            // Contravariant: t2 ≤ t1 (the expected param can be more specific)
1349            is_subtype_normalized(ctx, t2, t1) && {
1350                // Covariant: b1 ≤ b2 (alpha-rename to compare bodies, under the binder)
1351                let ext = ctx.extend(p1, (**t1).clone());
1352                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
1353                is_subtype_normalized(&ext, b1, &b2_renamed)
1354            }
1355        }
1356
1357        // Everything else is invariant: definitional equality.
1358        _ => def_eq_normalized(ctx, a, b),
1359    }
1360}
1361
1362/// Definitional equality (symmetric): reduction, congruence, η, and proof irrelevance. This
1363/// is the conversion used in INVARIANT positions (function arguments, `Lambda`/`Match`/`Fix`
1364/// subterms). `is_subtype` layers cumulativity on top of it.
1365pub(crate) fn def_eq(ctx: &Context, a: &Term, b: &Term) -> bool {
1366    if types_equal(a, b) {
1367        return true;
1368    }
1369    let a_norm = normalize(ctx, a);
1370    let b_norm = normalize(ctx, b);
1371    def_eq_normalized(ctx, &a_norm, &b_norm)
1372}
1373
1374/// Decompose a term into its head and argument spine (`f a b c` → `(f, [a,b,c])`).
1375fn spine_of(t: &Term) -> (&Term, Vec<&Term>) {
1376    let mut args = Vec::new();
1377    let mut head = t;
1378    while let Term::App(f, a) = head {
1379        args.push(a.as_ref());
1380        head = f;
1381    }
1382    args.reverse();
1383    (head, args)
1384}
1385
1386/// Structure η in ONE direction: if `mk_term` is a fully-applied constructor of a
1387/// registered structure `S` (`S_mk p̄ ā`) and `other` is not itself constructor-
1388/// headed, return `Some(eq)` where `eq` decides `mk_term ≡ other` by comparing each
1389/// field `aᵢ` against `S_projᵢ p̄ other`. `None` when the shape does not apply, so the
1390/// caller falls through to ordinary congruence.
1391fn try_structure_eta(ctx: &Context, mk_term: &Term, other: &Term) -> Option<bool> {
1392    let (head, args) = spine_of(mk_term);
1393    let Term::Global(hname) = head else { return None };
1394    let (_sname, info) = ctx.struct_of_constructor(hname)?;
1395    let nfields = info.projections.len();
1396    // Must be fully applied: params + one argument per field.
1397    if args.len() != info.num_params + nfields {
1398        return None;
1399    }
1400    // Do not eta when `other` is ALSO this constructor — that is ordinary
1401    // congruence (and avoids a pointless expansion loop).
1402    let (ohead, _) = spine_of(other);
1403    if matches!(ohead, Term::Global(n) if n == hname) {
1404        return None;
1405    }
1406    let params = &args[..info.num_params];
1407    let field_args = &args[info.num_params..];
1408    // Each field argument must equal the projection of `other`.
1409    Some(info.projections.iter().enumerate().all(|(i, proj)| {
1410        let mut proj_applied = Term::Global(proj.clone());
1411        for p in params {
1412            proj_applied = Term::App(Box::new(proj_applied), Box::new((*p).clone()));
1413        }
1414        proj_applied = Term::App(Box::new(proj_applied), Box::new(other.clone()));
1415        def_eq(ctx, field_args[i], &proj_applied)
1416    }))
1417}
1418
1419/// True if `t` is the head of a `Nat` Peano value — `Zero` or `Succ _` — the shape a
1420/// `Nat` literal bridges against (K6).
1421fn is_nat_peano_headed(t: &Term) -> bool {
1422    match t {
1423        Term::Global(n) => n == "Zero",
1424        Term::App(f, _) => matches!(f.as_ref(), Term::Global(n) if n == "Succ"),
1425        _ => false,
1426    }
1427}
1428
1429/// One Peano-unfolding step of a `Nat` literal: `Nat(0) → Zero`, `Nat(n) → Succ (Nat(n-1))`.
1430fn nat_peano_step(t: &Term) -> Term {
1431    match t {
1432        // `n ≤ 0` collapses to `Zero`, so peeling always TERMINATES even on a malformed
1433        // negative literal (a well-formed Nat is non-negative).
1434        Term::Lit(Literal::Nat(n)) if *n <= logicaffeine_base::BigInt::from_i64(0) => {
1435            Term::Global("Zero".to_string())
1436        }
1437        Term::Lit(Literal::Nat(n)) => Term::App(
1438            Box::new(Term::Global("Succ".to_string())),
1439            Box::new(Term::Lit(Literal::Nat(n.sub(&logicaffeine_base::BigInt::from_i64(1))))),
1440        ),
1441        other => other.clone(),
1442    }
1443}
1444
1445/// Definitional equality on already-normalized terms.
1446fn def_eq_normalized(ctx: &Context, a: &Term, b: &Term) -> bool {
1447    if types_equal(a, b) {
1448        return true;
1449    }
1450
1451    // η-conversion: `f ≡ λx. f x`. When exactly one side is a λ, compare its body against
1452    // the other side applied to the bound variable, under the binder.
1453    if let Term::Lambda { param, param_type, body } = a {
1454        if !matches!(b, Term::Lambda { .. }) {
1455            let ext = ctx.extend(param, (**param_type).clone());
1456            let bx = normalize(ctx, &Term::App(Box::new(b.clone()), Box::new(Term::Var(param.clone()))));
1457            return def_eq_normalized(&ext, body, &bx);
1458        }
1459    }
1460    if let Term::Lambda { param, param_type, body } = b {
1461        if !matches!(a, Term::Lambda { .. }) {
1462            let ext = ctx.extend(param, (**param_type).clone());
1463            let ax = normalize(ctx, &Term::App(Box::new(a.clone()), Box::new(Term::Var(param.clone()))));
1464            return def_eq_normalized(&ext, &ax, body);
1465        }
1466    }
1467
1468    // Structure η: `⟨p.1, …, p.n⟩ ≡ p`. When one side is a fully-applied
1469    // constructor of a REGISTERED structure and the other is not constructor-
1470    // headed, compare each field argument against the matching projection of the
1471    // other side. Keyed on the structure registry, so it never fires for an
1472    // ordinary inductive.
1473    if let Some(eq) = try_structure_eta(ctx, a, b) {
1474        return eq;
1475    }
1476    if let Some(eq) = try_structure_eta(ctx, b, a) {
1477        return eq;
1478    }
1479
1480    // Peano bridge (K6): a `Nat(n)` literal is definitionally `Succ^n Zero`. Two Nat
1481    // literals are equal iff their counts are; a Nat literal and a `Zero`/`Succ`-headed
1482    // Peano term are compared by peeling one `Succ` at a time (terminating at
1483    // `Nat(0) ≡ Zero`). Sound because the bridge unfolds to EXACTLY `Succ^n Zero`.
1484    match (a, b) {
1485        (Term::Lit(Literal::Nat(x)), Term::Lit(Literal::Nat(y))) => return x == y,
1486        (Term::Lit(Literal::Nat(_)), _) if is_nat_peano_headed(b) => {
1487            return def_eq_normalized(ctx, &nat_peano_step(a), b);
1488        }
1489        (_, Term::Lit(Literal::Nat(_))) if is_nat_peano_headed(a) => {
1490            return def_eq_normalized(ctx, a, &nat_peano_step(b));
1491        }
1492        _ => {}
1493    }
1494
1495    let congruent = match (a, b) {
1496        (Term::Sort(u1), Term::Sort(u2)) => u1.equiv(u2),
1497        (
1498            Term::Pi { param: p1, param_type: t1, body_type: b1 },
1499            Term::Pi { param: p2, param_type: t2, body_type: b2 },
1500        ) => {
1501            def_eq_normalized(ctx, t1, t2) && {
1502                let ext = ctx.extend(p1, (**t1).clone());
1503                let b2r = substitute(b2, p2, &Term::Var(p1.clone()));
1504                def_eq_normalized(&ext, b1, &b2r)
1505            }
1506        }
1507        (
1508            Term::Lambda { param: p1, param_type: t1, body: b1 },
1509            Term::Lambda { param: p2, param_type: t2, body: b2 },
1510        ) => {
1511            def_eq_normalized(ctx, t1, t2) && {
1512                let ext = ctx.extend(p1, (**t1).clone());
1513                let b2r = substitute(b2, p2, &Term::Var(p1.clone()));
1514                def_eq_normalized(&ext, b1, &b2r)
1515            }
1516        }
1517        (Term::App(f1, a1), Term::App(f2, a2)) => {
1518            def_eq_normalized(ctx, f1, f2) && def_eq_normalized(ctx, a1, a2)
1519        }
1520        (
1521            Term::Match { discriminant: d1, motive: m1, cases: c1 },
1522            Term::Match { discriminant: d2, motive: m2, cases: c2 },
1523        ) => {
1524            def_eq_normalized(ctx, d1, d2)
1525                && def_eq_normalized(ctx, m1, m2)
1526                && c1.len() == c2.len()
1527                && c1.iter().zip(c2.iter()).all(|(x, y)| def_eq_normalized(ctx, x, y))
1528        }
1529        (Term::Fix { name: n1, body: b1 }, Term::Fix { name: n2, body: b2 }) => {
1530            let b2r = substitute(b2, n2, &Term::Var(n1.clone()));
1531            def_eq_normalized(ctx, b1, &b2r)
1532        }
1533        _ => false,
1534    };
1535    if congruent {
1536        return true;
1537    }
1538
1539    // Proof irrelevance: any two proofs of the same proposition are equal. Fires only when
1540    // structural comparison fails (so it never costs on the common path).
1541    proof_irrelevant(ctx, a, b)
1542}
1543
1544/// Proof irrelevance: `a ≡ b` if `a`'s type is a proposition and `b` has a definitionally
1545/// equal type — i.e. both are proofs of the same `Prop`. Sound because `Prop` is a universe
1546/// of proof-irrelevant propositions.
1547fn proof_irrelevant(ctx: &Context, a: &Term, b: &Term) -> bool {
1548    let ta = match infer_type(ctx, a) {
1549        Ok(t) => t,
1550        Err(_) => return false,
1551    };
1552    // `a`'s type must itself be a proposition (`ta : Prop` or the definitionally-irrelevant
1553    // `ta : SProp`).
1554    match infer_type(ctx, &ta) {
1555        Ok(sort)
1556            if matches!(
1557                normalize(ctx, &sort),
1558                Term::Sort(Universe::Prop) | Term::Sort(Universe::SProp)
1559            ) => {}
1560        _ => return false,
1561    }
1562    // `b` must be a proof of a definitionally-equal proposition.
1563    match infer_type(ctx, b) {
1564        Ok(tb) => def_eq(ctx, &ta, &tb),
1565        Err(_) => false,
1566    }
1567}
1568
1569/// Extract the inductive type name from a type.
1570///
1571/// Handles both:
1572/// - Simple inductives: `Nat` → Some("Nat")
1573/// - Polymorphic inductives: `List A` → Some("List")
1574///
1575/// Returns None if the type is not an inductive type.
1576fn extract_inductive_name(ctx: &Context, ty: &Term) -> Option<String> {
1577    match ty {
1578        // Simple case: Global("Nat")
1579        Term::Global(name) if ctx.is_inductive(name) => Some(name.clone()),
1580
1581        // Polymorphic case: App(App(...App(Global("List"), _)...), _)
1582        // Recursively unwrap App to find the base Global
1583        Term::App(func, _) => extract_inductive_name(ctx, func),
1584
1585        _ => None,
1586    }
1587}
1588
1589/// Check if two types are equal (up to alpha-equivalence).
1590///
1591/// Two terms are alpha-equivalent if they are the same up to
1592/// renaming of bound variables.
1593fn types_equal(a: &Term, b: &Term) -> bool {
1594    // Hole matches anything (it's a type wildcard)
1595    if matches!(a, Term::Hole) || matches!(b, Term::Hole) {
1596        return true;
1597    }
1598
1599    match (a, b) {
1600        (Term::Sort(u1), Term::Sort(u2)) => u1.equiv(u2),
1601
1602        (Term::Lit(l1), Term::Lit(l2)) => l1 == l2,
1603
1604        (Term::Var(n1), Term::Var(n2)) => n1 == n2,
1605
1606        (Term::Global(n1), Term::Global(n2)) => n1 == n2,
1607
1608        (
1609            Term::Pi {
1610                param: p1,
1611                param_type: t1,
1612                body_type: b1,
1613            },
1614            Term::Pi {
1615                param: p2,
1616                param_type: t2,
1617                body_type: b2,
1618            },
1619        ) => {
1620            types_equal(t1, t2) && {
1621                // Alpha-equivalence: rename p2 to p1 in b2
1622                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
1623                types_equal(b1, &b2_renamed)
1624            }
1625        }
1626
1627        (
1628            Term::Lambda {
1629                param: p1,
1630                param_type: t1,
1631                body: b1,
1632            },
1633            Term::Lambda {
1634                param: p2,
1635                param_type: t2,
1636                body: b2,
1637            },
1638        ) => {
1639            types_equal(t1, t2) && {
1640                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
1641                types_equal(b1, &b2_renamed)
1642            }
1643        }
1644
1645        (Term::App(f1, a1), Term::App(f2, a2)) => types_equal(f1, f2) && types_equal(a1, a2),
1646
1647        (
1648            Term::Match {
1649                discriminant: d1,
1650                motive: m1,
1651                cases: c1,
1652            },
1653            Term::Match {
1654                discriminant: d2,
1655                motive: m2,
1656                cases: c2,
1657            },
1658        ) => {
1659            types_equal(d1, d2)
1660                && types_equal(m1, m2)
1661                && c1.len() == c2.len()
1662                && c1.iter().zip(c2.iter()).all(|(a, b)| types_equal(a, b))
1663        }
1664
1665        (
1666            Term::Fix {
1667                name: n1,
1668                body: b1,
1669            },
1670            Term::Fix {
1671                name: n2,
1672                body: b2,
1673            },
1674        ) => {
1675            // Alpha-equivalence: rename n2 to n1 in b2
1676            let b2_renamed = substitute(b2, n2, &Term::Var(n1.clone()));
1677            types_equal(b1, &b2_renamed)
1678        }
1679
1680        _ => false,
1681    }
1682}
1683
1684/// Whether a `Prop` inductive may be **large-eliminated** (into `Type`): true iff it is a subsingleton —
1685/// zero constructors, or exactly one whose non-parameter arguments all live in `Prop`. This is the CIC
1686/// elimination criterion that keeps `False` (ex falso), `And`, and `eq` eliminable while forbidding
1687/// multi-constructor propositions like `Or` and existentials carrying a `Type`-level witness.
1688pub(crate) fn is_subsingleton_prop(ctx: &Context, inductive_name: &str) -> KernelResult<bool> {
1689    let ctors = ctx.get_constructors(inductive_name);
1690    match ctors.len() {
1691        0 => Ok(true),
1692        1 => {
1693            let ctor_type = ctors[0].1.clone();
1694            let ind_type = ctx
1695                .get_global(inductive_name)
1696                .cloned()
1697                .ok_or_else(|| KernelError::UnboundVariable(inductive_name.to_string()))?;
1698            // The inductive's arity prefix (parameters + indices) is not "data"; only constructor
1699            // arguments beyond it carry content and must be propositional.
1700            let arity = pi_param_count(&ind_type);
1701            let mut local = ctx.clone();
1702            let mut t = ctor_type;
1703            let mut i = 0;
1704            while let Term::Pi { param, param_type, body_type } = t {
1705                if i >= arity && infer_sort(&local, &param_type)? != Universe::Prop {
1706                    return Ok(false);
1707                }
1708                local = local.extend(&param, (*param_type).clone());
1709                t = *body_type;
1710                i += 1;
1711            }
1712            Ok(true)
1713        }
1714        _ => Ok(false),
1715    }
1716}
1717
1718/// Count the leading `Π` binders of a type (an inductive's arity).
1719fn pi_param_count(ty: &Term) -> usize {
1720    match ty {
1721        Term::Pi { body_type, .. } => 1 + pi_param_count(body_type),
1722        _ => 0,
1723    }
1724}
1725
1726#[cfg(test)]
1727mod large_elim_tests {
1728    use super::*;
1729    use crate::context::Context;
1730    use crate::prelude::StandardLibrary;
1731    use crate::term::{Term, Universe};
1732
1733    fn g(s: &str) -> Term { Term::Global(s.to_string()) }
1734    fn app(f: Term, x: Term) -> Term { Term::App(Box::new(f), Box::new(x)) }
1735    fn lam(p: &str, ty: Term, body: Term) -> Term {
1736        Term::Lambda { param: p.to_string(), param_type: Box::new(ty), body: Box::new(body) }
1737    }
1738    fn or_tt() -> Term { app(app(g("Or"), g("True")), g("True")) }
1739
1740    /// CRITIQUE #1 (open half): CIC large-elimination restriction. Matching a proof of `Or` (a Prop with
1741    /// TWO constructors) into `Type` (here returning `Nat`) extracts computational content from a proof
1742    /// and breaks consistency — the kernel MUST reject it.
1743    #[test]
1744    fn large_elimination_of_or_into_type_is_rejected() {
1745        let mut ctx = Context::new();
1746        StandardLibrary::register(&mut ctx);
1747        let case = lam("_", g("True"), g("Zero")); // λ_:True. Zero  (Zero : Nat)
1748        let m = Term::Match {
1749            discriminant: Box::new(Term::Var("h".to_string())),
1750            motive: Box::new(lam("_", or_tt(), g("Nat"))), // λ_:Or True True. Nat  (large)
1751            cases: vec![case.clone(), case],
1752        };
1753        let term = lam("h", or_tt(), m);
1754        assert!(
1755            infer_type(&ctx, &term).is_err(),
1756            "large elimination of Or (2 constructors) into Type must be rejected for consistency"
1757        );
1758    }
1759
1760    /// Regression: `ex falso` — large elimination of `False` (ZERO constructors, a subsingleton) into any
1761    /// type — MUST stay legal, or every proof-by-contradiction breaks.
1762    #[test]
1763    fn ex_falso_large_elimination_of_false_still_allowed() {
1764        let mut ctx = Context::new();
1765        StandardLibrary::register(&mut ctx);
1766        let m = Term::Match {
1767            discriminant: Box::new(Term::Var("h".to_string())),
1768            motive: Box::new(lam("_", g("False"), g("Nat"))), // λ_:False. Nat (large, but False is empty)
1769            cases: vec![],
1770        };
1771        let term = lam("h", g("False"), m);
1772        assert!(infer_type(&ctx, &term).is_ok(), "ex falso (large elim of empty False) must stay legal");
1773    }
1774
1775    /// Regression: SMALL elimination of `Or` (into a `Prop`) is always fine — the restriction must not
1776    /// over-reach and reject ordinary propositional case analysis.
1777    #[test]
1778    fn small_elimination_of_or_into_prop_still_allowed() {
1779        let mut ctx = Context::new();
1780        StandardLibrary::register(&mut ctx);
1781        let case = lam("_", g("True"), g("I")); // λ_:True. I  (I : True)
1782        let m = Term::Match {
1783            discriminant: Box::new(Term::Var("h".to_string())),
1784            motive: Box::new(lam("_", or_tt(), g("True"))), // λ_:Or True True. True  (small, Prop)
1785            cases: vec![case.clone(), case],
1786        };
1787        let term = lam("h", or_tt(), m);
1788        assert!(infer_type(&ctx, &term).is_ok(), "small elimination of Or into Prop must stay legal");
1789    }
1790}