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, ¶m_type)?;
164
165 // Substitute argument into body type
166 Ok(substitute(&body_type, ¶m, 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, ¶m_type)? != Universe::Prop {
1706 return Ok(false);
1707 }
1708 local = local.extend(¶m, (*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}