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        // Pi: Π(x:A). B : Type max(sort(A), sort(B))
69        Term::Pi {
70            param,
71            param_type,
72            body_type,
73        } => {
74            // A must be a type
75            let a_sort = infer_sort(ctx, param_type)?;
76
77            // B must be a type in the extended context
78            let extended_ctx = ctx.extend(param, (**param_type).clone());
79            let b_sort = infer_sort(&extended_ctx, body_type)?;
80
81            // The Pi type lives in the max of the two universes
82            Ok(Term::Sort(a_sort.max(&b_sort)))
83        }
84
85        // Lambda: λ(x:A). t : Π(x:A). T where t : T
86        Term::Lambda {
87            param,
88            param_type,
89            body,
90        } => {
91            // Check param_type is well-formed (is a type)
92            let _ = infer_sort(ctx, param_type)?;
93
94            // Infer body type in extended context
95            let extended_ctx = ctx.extend(param, (**param_type).clone());
96            let body_type = infer_type(&extended_ctx, body)?;
97
98            // The lambda has a Pi type
99            Ok(Term::Pi {
100                param: param.clone(),
101                param_type: param_type.clone(),
102                body_type: Box::new(body_type),
103            })
104        }
105
106        // App: (f a) : B[x := a] where f : Π(x:A). B and a : A
107        Term::App(func, arg) => {
108            let func_type = infer_type(ctx, func)?;
109
110            match func_type {
111                Term::Pi {
112                    param,
113                    param_type,
114                    body_type,
115                } => {
116                    // Check argument has expected type
117                    check_type(ctx, arg, &param_type)?;
118
119                    // Substitute argument into body type
120                    Ok(substitute(&body_type, &param, arg))
121                }
122                _ => Err(KernelError::NotAFunction(format!("{}", func)))
123            }
124        }
125
126        // Match: pattern matching on inductive types
127        Term::Match {
128            discriminant,
129            motive,
130            cases,
131        } => {
132            // 1. Discriminant must have an inductive type
133            let disc_type = infer_type(ctx, discriminant)?;
134            let inductive_name = extract_inductive_name(ctx, &disc_type)
135                .ok_or_else(|| KernelError::NotAnInductive(format!("{}", disc_type)))?;
136
137            // 2. Check motive is well-formed
138            // The motive can be either:
139            // - A function λ_:I. T (proper motive)
140            // - A raw type T (constant motive, wrapped automatically)
141            let motive_type = infer_type(ctx, motive)?;
142            let effective_motive = match &motive_type {
143                Term::Pi {
144                    param_type,
145                    body_type,
146                    ..
147                } => {
148                    // Motive is a function - check it takes the inductive type
149                    if !types_equal(param_type, &disc_type) {
150                        return Err(KernelError::InvalidMotive(format!(
151                            "motive parameter {} doesn't match discriminant type {}",
152                            param_type, disc_type
153                        )));
154                    }
155                    // body_type should be a Sort
156                    match infer_type(ctx, body_type) {
157                        Ok(Term::Sort(_)) => {}
158                        _ => {
159                            return Err(KernelError::InvalidMotive(format!(
160                                "motive body {} is not a type",
161                                body_type
162                            )));
163                        }
164                    }
165                    // Use motive as-is
166                    (**motive).clone()
167                }
168                Term::Sort(_) => {
169                    // Motive is a raw type - wrap in a constant function
170                    // λ_:disc_type. motive
171                    Term::Lambda {
172                        param: "_".to_string(),
173                        param_type: Box::new(disc_type.clone()),
174                        body: motive.clone(),
175                    }
176                }
177                _ => {
178                    return Err(KernelError::InvalidMotive(format!(
179                        "motive {} is not a function or type",
180                        motive
181                    )));
182                }
183            };
184
185            // 3. Check case count matches constructor count
186            let constructors = ctx.get_constructors(&inductive_name);
187            if cases.len() != constructors.len() {
188                return Err(KernelError::WrongNumberOfCases {
189                    expected: constructors.len(),
190                    found: cases.len(),
191                });
192            }
193
194            // 4. Check each case has the correct type
195            for (case, (ctor_name, ctor_type)) in cases.iter().zip(constructors.iter()) {
196                let expected_case_type = compute_case_type(&effective_motive, ctor_name, ctor_type, &disc_type);
197                check_type(ctx, case, &expected_case_type)?;
198            }
199
200            // 5. Return type is Motive(discriminant), beta-reduced
201            // Without beta reduction, (λ_:T. R) x returns the un-reduced form
202            // which causes type mismatches in nested matches.
203            let return_type = Term::App(
204                Box::new(effective_motive),
205                discriminant.clone(),
206            );
207            Ok(beta_reduce(&return_type))
208        }
209
210        // Literal: infer type based on literal kind
211        Term::Lit(lit) => {
212            match lit {
213                Literal::Int(_) => Ok(Term::Global("Int".to_string())),
214                Literal::Float(_) => Ok(Term::Global("Float".to_string())),
215                Literal::Text(_) => Ok(Term::Global("Text".to_string())),
216            }
217        }
218
219        // Hole: implicit argument, cannot infer type standalone
220        // Holes are handled specially in check_type
221        Term::Hole => Err(KernelError::CannotInferHole),
222
223        // Fix: fix f. body
224        // The type of (fix f. body) is the type of body when f is bound to that type.
225        // This is a fixpoint equation: T = type_of(body) where f : T.
226        //
227        // For typical fixpoints, body is a lambda: fix f. λx:A. e
228        // The type is Π(x:A). B where B is the type of e (with f : Π(x:A). B).
229        Term::Fix { name, body } => {
230            // For fix f. body, we need to handle the recursive reference to f.
231            // We structurally infer the type from lambda structure.
232            //
233            // This works because:
234            // 1. The body is typically nested lambdas with explicit parameter types
235            // 2. The return type is determined by the innermost expression's motive
236            // 3. Recursive calls have the same type as the fixpoint itself
237
238            // Extract the structural type from nested lambdas and motive
239            let structural_type = infer_fix_type_structurally(ctx, body)?;
240
241            // *** THE GUARDIAN: TERMINATION CHECK ***
242            // Verify that recursive calls decrease structurally.
243            // Without this check, we could "prove" False by looping forever.
244            crate::termination::check_termination(ctx, name, body)?;
245
246            // Sanity check: verify the body is well-formed with f bound
247            let extended = ctx.extend(name, structural_type.clone());
248            let _ = infer_type(&extended, body)?;
249
250            Ok(structural_type)
251        }
252    }
253}
254
255/// Infer the type of a fixpoint body structurally.
256///
257/// For `λx:A. body`, returns `Π(x:A). <type of body>`.
258/// This recursively handles nested lambdas.
259///
260/// The key insight is that for well-formed fixpoints, the body structure
261/// determines the type: parameters have explicit types, and the return type
262/// can be inferred from the innermost expression.
263fn infer_fix_type_structurally(ctx: &Context, term: &Term) -> KernelResult<Term> {
264    match term {
265        Term::Lambda {
266            param,
267            param_type,
268            body,
269        } => {
270            // Check param_type is well-formed
271            let _ = infer_sort(ctx, param_type)?;
272
273            // Extend context and recurse into body
274            let extended = ctx.extend(param, (**param_type).clone());
275            let body_type = infer_fix_type_structurally(&extended, body)?;
276
277            // Build Pi type
278            Ok(Term::Pi {
279                param: param.clone(),
280                param_type: param_type.clone(),
281                body_type: Box::new(body_type),
282            })
283        }
284        // For non-lambda bodies (the base case), we need to determine the return type.
285        // This is typically a Match expression whose motive determines the return type.
286        Term::Match { motive, .. } => {
287            // The motive λx:I. T tells us the return type when applied to discriminant.
288            // For a simple motive λ_. Nat, the return type is Nat.
289            // We extract the body of the motive as the return type.
290            if let Term::Lambda { body, .. } = motive.as_ref() {
291                Ok((**body).clone())
292            } else {
293                // Motive is a raw type (constant motive) - return it directly
294                // This handles cases like `match xs return Nat with ...`
295                // where the return type is just `Nat`
296                Ok((**motive).clone())
297            }
298        }
299        // For other expressions, try normal inference
300        _ => infer_type(ctx, term),
301    }
302}
303
304/// Check that a term has the expected type (with subtyping/cumulativity).
305///
306/// Implements bidirectional type checking: when checking a Lambda against a Pi,
307/// we can use the Pi's parameter type instead of the Lambda's (which may be a
308/// placeholder from match case parsing).
309fn check_type(ctx: &Context, term: &Term, expected: &Term) -> KernelResult<()> {
310    // Hole as term: accept if expected is a Sort (Hole stands for a type)
311    // This allows `Eq Hole X Y` where Eq expects Type as first arg
312    if matches!(term, Term::Hole) {
313        if matches!(expected, Term::Sort(_)) {
314            return Ok(());
315        }
316        return Err(KernelError::TypeMismatch {
317            expected: format!("{}", expected),
318            found: "_".to_string(),
319        });
320    }
321
322    // Hole as expected type: accept any well-typed term
323    // This allows checking args against Hole in `(Eq Hole) X Y` intermediates
324    if matches!(expected, Term::Hole) {
325        let _ = infer_type(ctx, term)?; // Just verify term is well-typed
326        return Ok(());
327    }
328
329    // Special case: Lambda with placeholder type checked against Pi
330    // This handles match cases where binder types come from the expected type
331    if let Term::Lambda {
332        param,
333        param_type,
334        body,
335    } = term
336    {
337        // Check if param_type is a placeholder ("_")
338        if let Term::Global(name) = param_type.as_ref() {
339            if name == "_" {
340                // Bidirectional mode: get param type from expected
341                if let Term::Pi {
342                    param_type: expected_param_type,
343                    body_type: expected_body_type,
344                    param: expected_param,
345                } = expected
346                {
347                    // Check body in extended context using expected param type
348                    let extended_ctx = ctx.extend(param, (**expected_param_type).clone());
349                    // Substitute in expected_body_type if param names differ
350                    let body_expected = if param != expected_param {
351                        substitute(expected_body_type, expected_param, &Term::Var(param.clone()))
352                    } else {
353                        (**expected_body_type).clone()
354                    };
355                    return check_type(&extended_ctx, body, &body_expected);
356                }
357            }
358        }
359    }
360
361    let inferred = infer_type(ctx, term)?;
362    if is_subtype(ctx, &inferred, expected) {
363        Ok(())
364    } else {
365        Err(KernelError::TypeMismatch {
366            expected: format!("{}", expected),
367            found: format!("{}", inferred),
368        })
369    }
370}
371
372/// Infer the sort (universe) of a type.
373///
374/// A term is a type if its type is a Sort.
375fn infer_sort(ctx: &Context, term: &Term) -> KernelResult<Universe> {
376    let ty = infer_type(ctx, term)?;
377    match ty {
378        Term::Sort(u) => Ok(u),
379        _ => Err(KernelError::NotAType(format!("{}", term))),
380    }
381}
382
383/// Beta-reduce a term (single step, at the head).
384///
385/// (λx.body) arg → body[x := arg]
386fn beta_reduce(term: &Term) -> Term {
387    match term {
388        Term::App(func, arg) => {
389            match func.as_ref() {
390                Term::Lambda { param, body, .. } => {
391                    // Beta reduction: (λx.body) arg → body[x := arg]
392                    substitute(body, param, arg)
393                }
394                _ => term.clone(),
395            }
396        }
397        _ => term.clone(),
398    }
399}
400
401/// Compute the expected type for a match case.
402///
403/// For a constructor C : A₁ → A₂ → ... → I,
404/// the case type is: Πa₁:A₁. Πa₂:A₂. ... P(C a₁ a₂ ...)
405///
406/// For a zero-argument constructor like Zero : Nat,
407/// the case type is just P(Zero).
408///
409/// For polymorphic constructors like Nil : Π(A:Type). List A,
410/// when matching on `xs : List A`, we skip the type parameter
411/// and use the instantiated type argument instead.
412fn compute_case_type(motive: &Term, ctor_name: &str, ctor_type: &Term, disc_type: &Term) -> Term {
413    // Extract type arguments from discriminant type
414    // e.g., List A → [A], List → []
415    let type_args = extract_type_args(disc_type);
416    let num_type_args = type_args.len();
417
418    // Collect parameters from constructor type
419    let mut all_params: Vec<(String, Term)> = Vec::new();
420    let mut current = ctor_type;
421
422    while let Term::Pi {
423        param,
424        param_type,
425        body_type,
426    } = current
427    {
428        all_params.push((param.clone(), (**param_type).clone()));
429        current = body_type;
430    }
431
432    // Split into type parameters (to skip) and value parameters (to bind)
433    let (type_params, value_params): (Vec<_>, Vec<_>) = all_params
434        .into_iter()
435        .enumerate()
436        .partition(|(i, _)| *i < num_type_args);
437
438    // Generate unique names for value parameters to avoid shadowing issues
439    // Use pattern: __arg0, __arg1, etc.
440    let named_value_params: Vec<(usize, (String, Term))> = value_params
441        .into_iter()
442        .enumerate()
443        .map(|(i, (idx, (_, param_type)))| {
444            (idx, (format!("__arg{}", i), param_type))
445        })
446        .collect();
447
448    // Build C(type_args..., value_params...)
449    let mut ctor_applied = Term::Global(ctor_name.to_string());
450
451    // Apply type arguments (from discriminant type)
452    for type_arg in &type_args {
453        ctor_applied = Term::App(Box::new(ctor_applied), Box::new(type_arg.clone()));
454    }
455
456    // Apply value parameters (as bound variables with unique names)
457    for (_, (param_name, _)) in &named_value_params {
458        ctor_applied = Term::App(
459            Box::new(ctor_applied),
460            Box::new(Term::Var(param_name.clone())),
461        );
462    }
463
464    // Build P(C type_args value_params) and beta-reduce it
465    let motive_applied = Term::App(Box::new(motive.clone()), Box::new(ctor_applied));
466    let result_type = beta_reduce(&motive_applied);
467
468    // Wrap in Πa₁:A₁. Πa₂:A₂. ... for value parameters only
469    // (in reverse order to get correct nesting)
470    let mut case_type = result_type;
471    for (_, (param_name, param_type)) in named_value_params.into_iter().rev() {
472        // Substitute type arguments into parameter type
473        let mut subst_param_type = param_type;
474        for ((_, (type_param_name, _)), type_arg) in type_params.iter().zip(type_args.iter()) {
475            subst_param_type = substitute(&subst_param_type, type_param_name, type_arg);
476        }
477
478        case_type = Term::Pi {
479            param: param_name,
480            param_type: Box::new(subst_param_type),
481            body_type: Box::new(case_type),
482        };
483    }
484
485    case_type
486}
487
488/// Extract type arguments from a type application.
489///
490/// - `List A` → `[A]`
491/// - `Either A B` → `[A, B]`
492/// - `Nat` → `[]`
493fn extract_type_args(ty: &Term) -> Vec<Term> {
494    let mut args = Vec::new();
495    let mut current = ty;
496
497    while let Term::App(func, arg) = current {
498        args.push((**arg).clone());
499        current = func;
500    }
501
502    args.reverse();
503    args
504}
505
506/// Substitute a term for a variable: `body[var := replacement]`.
507///
508/// Performs capture-avoiding substitution. Variables bound by lambda,
509/// pi, or fix that shadow `var` are not substituted into.
510///
511/// # Capture Avoidance
512///
513/// Given `substitute(λx. y, "y", x)`, the result is `λx. x` (not `λx. x`
514/// with the inner x captured). This implementation relies on unique
515/// variable names from parsing.
516///
517/// # Term Forms
518///
519/// - `Sort`, `Lit`, `Hole`, `Global` - Unchanged (no variables)
520/// - `Var(name)` - Replaced if `name == var`, unchanged otherwise
521/// - `Pi`, `Lambda`, `Fix` - Substitute in components, respecting shadowing
522/// - `App`, `Match` - Substitute recursively in all subterms
523pub fn substitute(body: &Term, var: &str, replacement: &Term) -> Term {
524    match body {
525        Term::Sort(u) => Term::Sort(u.clone()),
526
527        // Literals are never substituted
528        Term::Lit(lit) => Term::Lit(lit.clone()),
529
530        // Holes are never substituted (they're implicit type placeholders)
531        Term::Hole => Term::Hole,
532
533        Term::Var(name) if name == var => replacement.clone(),
534        Term::Var(name) => Term::Var(name.clone()),
535
536        // Globals are never substituted (they're not bound variables)
537        Term::Global(name) => Term::Global(name.clone()),
538
539        Term::Pi {
540            param,
541            param_type,
542            body_type,
543        } => {
544            let new_param_type = substitute(param_type, var, replacement);
545            // Don't substitute in body if the parameter shadows var
546            let new_body_type = if param == var {
547                (**body_type).clone()
548            } else {
549                substitute(body_type, var, replacement)
550            };
551            Term::Pi {
552                param: param.clone(),
553                param_type: Box::new(new_param_type),
554                body_type: Box::new(new_body_type),
555            }
556        }
557
558        Term::Lambda {
559            param,
560            param_type,
561            body,
562        } => {
563            let new_param_type = substitute(param_type, var, replacement);
564            // Don't substitute in body if the parameter shadows var
565            let new_body = if param == var {
566                (**body).clone()
567            } else {
568                substitute(body, var, replacement)
569            };
570            Term::Lambda {
571                param: param.clone(),
572                param_type: Box::new(new_param_type),
573                body: Box::new(new_body),
574            }
575        }
576
577        Term::App(func, arg) => Term::App(
578            Box::new(substitute(func, var, replacement)),
579            Box::new(substitute(arg, var, replacement)),
580        ),
581
582        Term::Match {
583            discriminant,
584            motive,
585            cases,
586        } => Term::Match {
587            discriminant: Box::new(substitute(discriminant, var, replacement)),
588            motive: Box::new(substitute(motive, var, replacement)),
589            cases: cases
590                .iter()
591                .map(|c| substitute(c, var, replacement))
592                .collect(),
593        },
594
595        Term::Fix { name, body } => {
596            // Don't substitute in body if the fixpoint name shadows var
597            if name == var {
598                Term::Fix {
599                    name: name.clone(),
600                    body: body.clone(),
601                }
602            } else {
603                Term::Fix {
604                    name: name.clone(),
605                    body: Box::new(substitute(body, var, replacement)),
606                }
607            }
608        }
609    }
610}
611
612/// Check if type `a` is a subtype of type `b` (cumulativity).
613///
614/// Implements the subtyping relation for the Calculus of Constructions
615/// with cumulative universes.
616///
617/// # Subtyping Rules
618///
619/// - **Universe cumulativity**: `Type i <= Type j` if `i <= j`
620/// - **Pi contravariance**: `Π(x:A). B <= Π(x:A'). B'` if `A' <= A` and `B <= B'`
621/// - **Structural equality**: Other terms are compared after normalization
622///
623/// # Normalization
624///
625/// Both types are normalized before comparison, ensuring that definitionally
626/// equal types are recognized as subtypes.
627///
628/// # Cumulativity Examples
629///
630/// - `Type 0 <= Type 1` (lower universe is subtype of higher)
631/// - `Nat -> Type 0 <= Nat -> Type 1` (covariant in return type)
632/// - `Type 1 -> Nat <= Type 0 -> Nat` (contravariant in parameter type)
633pub fn is_subtype(ctx: &Context, a: &Term, b: &Term) -> bool {
634    // Normalize both terms before comparison
635    // This ensures that e.g. `ReachesOne (collatzStep 2)` equals `ReachesOne 1`
636    let a_norm = normalize(ctx, a);
637    let b_norm = normalize(ctx, b);
638
639    is_subtype_normalized(ctx, &a_norm, &b_norm)
640}
641
642/// Check subtyping on already-normalized terms.
643fn is_subtype_normalized(ctx: &Context, a: &Term, b: &Term) -> bool {
644    match (a, b) {
645        // Universe subtyping
646        (Term::Sort(u1), Term::Sort(u2)) => u1.is_subtype_of(u2),
647
648        // Pi subtyping (contravariant in param, covariant in body)
649        (
650            Term::Pi {
651                param: p1,
652                param_type: t1,
653                body_type: b1,
654            },
655            Term::Pi {
656                param: p2,
657                param_type: t2,
658                body_type: b2,
659            },
660        ) => {
661            // Contravariant: t2 ≤ t1 (the expected param can be more specific)
662            is_subtype_normalized(ctx, t2, t1) && {
663                // Covariant: b1 ≤ b2 (alpha-rename to compare bodies)
664                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
665                is_subtype_normalized(ctx, b1, &b2_renamed)
666            }
667        }
668
669        // Fall back to structural equality for other terms
670        _ => types_equal(a, b),
671    }
672}
673
674/// Extract the inductive type name from a type.
675///
676/// Handles both:
677/// - Simple inductives: `Nat` → Some("Nat")
678/// - Polymorphic inductives: `List A` → Some("List")
679///
680/// Returns None if the type is not an inductive type.
681fn extract_inductive_name(ctx: &Context, ty: &Term) -> Option<String> {
682    match ty {
683        // Simple case: Global("Nat")
684        Term::Global(name) if ctx.is_inductive(name) => Some(name.clone()),
685
686        // Polymorphic case: App(App(...App(Global("List"), _)...), _)
687        // Recursively unwrap App to find the base Global
688        Term::App(func, _) => extract_inductive_name(ctx, func),
689
690        _ => None,
691    }
692}
693
694/// Check if two types are equal (up to alpha-equivalence).
695///
696/// Two terms are alpha-equivalent if they are the same up to
697/// renaming of bound variables.
698fn types_equal(a: &Term, b: &Term) -> bool {
699    // Hole matches anything (it's a type wildcard)
700    if matches!(a, Term::Hole) || matches!(b, Term::Hole) {
701        return true;
702    }
703
704    match (a, b) {
705        (Term::Sort(u1), Term::Sort(u2)) => u1 == u2,
706
707        (Term::Lit(l1), Term::Lit(l2)) => l1 == l2,
708
709        (Term::Var(n1), Term::Var(n2)) => n1 == n2,
710
711        (Term::Global(n1), Term::Global(n2)) => n1 == n2,
712
713        (
714            Term::Pi {
715                param: p1,
716                param_type: t1,
717                body_type: b1,
718            },
719            Term::Pi {
720                param: p2,
721                param_type: t2,
722                body_type: b2,
723            },
724        ) => {
725            types_equal(t1, t2) && {
726                // Alpha-equivalence: rename p2 to p1 in b2
727                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
728                types_equal(b1, &b2_renamed)
729            }
730        }
731
732        (
733            Term::Lambda {
734                param: p1,
735                param_type: t1,
736                body: b1,
737            },
738            Term::Lambda {
739                param: p2,
740                param_type: t2,
741                body: b2,
742            },
743        ) => {
744            types_equal(t1, t2) && {
745                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
746                types_equal(b1, &b2_renamed)
747            }
748        }
749
750        (Term::App(f1, a1), Term::App(f2, a2)) => types_equal(f1, f2) && types_equal(a1, a2),
751
752        (
753            Term::Match {
754                discriminant: d1,
755                motive: m1,
756                cases: c1,
757            },
758            Term::Match {
759                discriminant: d2,
760                motive: m2,
761                cases: c2,
762            },
763        ) => {
764            types_equal(d1, d2)
765                && types_equal(m1, m2)
766                && c1.len() == c2.len()
767                && c1.iter().zip(c2.iter()).all(|(a, b)| types_equal(a, b))
768        }
769
770        (
771            Term::Fix {
772                name: n1,
773                body: b1,
774            },
775            Term::Fix {
776                name: n2,
777                body: b2,
778            },
779        ) => {
780            // Alpha-equivalence: rename n2 to n1 in b2
781            let b2_renamed = substitute(b2, n2, &Term::Var(n1.clone()));
782            types_equal(b1, &b2_renamed)
783        }
784
785        _ => false,
786    }
787}