Skip to main content

logicaffeine_kernel/
type_checker.rs

1//! Bidirectional type checker for the Calculus of Constructions.
2//!
3//! The type checker implements the typing rules of CoC:
4//!
5//! ```text
6//! ─────────────────────── (Sort)
7//!   Γ ⊢ Type n : Type (n+1)
8//!
9//!   Γ(x) = A
10//! ─────────────── (Var)
11//!   Γ ⊢ x : A
12//!
13//!   Γ ⊢ A : Type i    Γ, x:A ⊢ B : Type j
14//! ─────────────────────────────────────────── (Pi)
15//!          Γ ⊢ Π(x:A). B : Type max(i,j)
16//!
17//!   Γ ⊢ A : Type i    Γ, x:A ⊢ t : B
18//! ─────────────────────────────────────── (Lambda)
19//!      Γ ⊢ λ(x:A). t : Π(x:A). B
20//!
21//!   Γ ⊢ f : Π(x:A). B    Γ ⊢ a : A
22//! ───────────────────────────────────── (App)
23//!         Γ ⊢ f a : B[x := a]
24//! ```
25
26use crate::context::Context;
27use crate::error::{KernelError, KernelResult};
28use crate::reduction::normalize;
29use crate::term::{Literal, Term, Universe};
30
31/// Infer the type of a term in a context.
32///
33/// This is the main entry point for type checking. It implements bidirectional
34/// type inference for the Calculus of Constructions.
35///
36/// # Type Rules
37///
38/// - `Type n : Type (n+1)` - Universes form a hierarchy
39/// - `x : A` if `x : A` in context - Variable lookup
40/// - `Π(x:A). B : Type max(i,j)` if `A : Type i` and `B : Type j`
41/// - `λ(x:A). t : Π(x:A). B` if `t : B` in extended context
42/// - `f a : B[x := a]` if `f : Π(x:A). B` and `a : A`
43///
44/// # Errors
45///
46/// Returns [`KernelError`] variants for:
47/// - Unbound variables
48/// - Type mismatches in applications
49/// - Invalid match constructs
50/// - Termination check failures for fixpoints
51pub fn infer_type(ctx: &Context, term: &Term) -> KernelResult<Term> {
52    match term {
53        // Sort: Type n : Type (n+1)
54        Term::Sort(u) => Ok(Term::Sort(u.succ())),
55
56        // Var: lookup in local context
57        Term::Var(name) => ctx
58            .get(name)
59            .cloned()
60            .ok_or_else(|| KernelError::UnboundVariable(name.clone())),
61
62        // Global: lookup in global context (inductives and constructors)
63        Term::Global(name) => ctx
64            .get_global(name)
65            .cloned()
66            .ok_or_else(|| KernelError::UnboundVariable(name.clone())),
67
68        // 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                Literal::Duration(_) => Ok(Term::Global("Duration".to_string())),
217                Literal::Date(_) => Ok(Term::Global("Date".to_string())),
218                Literal::Moment(_) => Ok(Term::Global("Moment".to_string())),
219            }
220        }
221
222        // Hole: implicit argument, cannot infer type standalone
223        // Holes are handled specially in check_type
224        Term::Hole => Err(KernelError::CannotInferHole),
225
226        // Fix: fix f. body
227        // The type of (fix f. body) is the type of body when f is bound to that type.
228        // This is a fixpoint equation: T = type_of(body) where f : T.
229        //
230        // For typical fixpoints, body is a lambda: fix f. λx:A. e
231        // The type is Π(x:A). B where B is the type of e (with f : Π(x:A). B).
232        Term::Fix { name, body } => {
233            // For fix f. body, we need to handle the recursive reference to f.
234            // We structurally infer the type from lambda structure.
235            //
236            // This works because:
237            // 1. The body is typically nested lambdas with explicit parameter types
238            // 2. The return type is determined by the innermost expression's motive
239            // 3. Recursive calls have the same type as the fixpoint itself
240
241            // Extract the structural type from nested lambdas and motive
242            let structural_type = infer_fix_type_structurally(ctx, body)?;
243
244            // *** THE GUARDIAN: TERMINATION CHECK ***
245            // Verify that recursive calls decrease structurally.
246            // Without this check, we could "prove" False by looping forever.
247            crate::termination::check_termination(ctx, name, body)?;
248
249            // Sanity check: verify the body is well-formed with f bound
250            let extended = ctx.extend(name, structural_type.clone());
251            let _ = infer_type(&extended, body)?;
252
253            Ok(structural_type)
254        }
255    }
256}
257
258/// Infer the type of a fixpoint body structurally.
259///
260/// For `λx:A. body`, returns `Π(x:A). <type of body>`.
261/// This recursively handles nested lambdas.
262///
263/// The key insight is that for well-formed fixpoints, the body structure
264/// determines the type: parameters have explicit types, and the return type
265/// can be inferred from the innermost expression.
266fn infer_fix_type_structurally(ctx: &Context, term: &Term) -> KernelResult<Term> {
267    match term {
268        Term::Lambda {
269            param,
270            param_type,
271            body,
272        } => {
273            // Check param_type is well-formed
274            let _ = infer_sort(ctx, param_type)?;
275
276            // Extend context and recurse into body
277            let extended = ctx.extend(param, (**param_type).clone());
278            let body_type = infer_fix_type_structurally(&extended, body)?;
279
280            // Build Pi type
281            Ok(Term::Pi {
282                param: param.clone(),
283                param_type: param_type.clone(),
284                body_type: Box::new(body_type),
285            })
286        }
287        // For non-lambda bodies (the base case), we need to determine the return type.
288        // This is typically a Match expression whose motive determines the return type.
289        Term::Match { motive, .. } => {
290            // The motive λx:I. T tells us the return type when applied to discriminant.
291            // For a simple motive λ_. Nat, the return type is Nat.
292            // We extract the body of the motive as the return type.
293            if let Term::Lambda { body, .. } = motive.as_ref() {
294                Ok((**body).clone())
295            } else {
296                // Motive is a raw type (constant motive) - return it directly
297                // This handles cases like `match xs return Nat with ...`
298                // where the return type is just `Nat`
299                Ok((**motive).clone())
300            }
301        }
302        // For other expressions, try normal inference
303        _ => infer_type(ctx, term),
304    }
305}
306
307/// Check that a term has the expected type (with subtyping/cumulativity).
308///
309/// Implements bidirectional type checking: when checking a Lambda against a Pi,
310/// we can use the Pi's parameter type instead of the Lambda's (which may be a
311/// placeholder from match case parsing).
312fn check_type(ctx: &Context, term: &Term, expected: &Term) -> KernelResult<()> {
313    // Hole as term: accept if expected is a Sort (Hole stands for a type)
314    // This allows `Eq Hole X Y` where Eq expects Type as first arg
315    if matches!(term, Term::Hole) {
316        if matches!(expected, Term::Sort(_)) {
317            return Ok(());
318        }
319        return Err(KernelError::TypeMismatch {
320            expected: format!("{}", expected),
321            found: "_".to_string(),
322        });
323    }
324
325    // Hole as expected type: accept any well-typed term
326    // This allows checking args against Hole in `(Eq Hole) X Y` intermediates
327    if matches!(expected, Term::Hole) {
328        let _ = infer_type(ctx, term)?; // Just verify term is well-typed
329        return Ok(());
330    }
331
332    // Special case: Lambda with placeholder type checked against Pi
333    // This handles match cases where binder types come from the expected type
334    if let Term::Lambda {
335        param,
336        param_type,
337        body,
338    } = term
339    {
340        // Check if param_type is a placeholder ("_")
341        if let Term::Global(name) = param_type.as_ref() {
342            if name == "_" {
343                // Bidirectional mode: get param type from expected
344                if let Term::Pi {
345                    param_type: expected_param_type,
346                    body_type: expected_body_type,
347                    param: expected_param,
348                } = expected
349                {
350                    // Check body in extended context using expected param type
351                    let extended_ctx = ctx.extend(param, (**expected_param_type).clone());
352                    // Substitute in expected_body_type if param names differ
353                    let body_expected = if param != expected_param {
354                        substitute(expected_body_type, expected_param, &Term::Var(param.clone()))
355                    } else {
356                        (**expected_body_type).clone()
357                    };
358                    return check_type(&extended_ctx, body, &body_expected);
359                }
360            }
361        }
362    }
363
364    let inferred = infer_type(ctx, term)?;
365    if is_subtype(ctx, &inferred, expected) {
366        Ok(())
367    } else {
368        Err(KernelError::TypeMismatch {
369            expected: format!("{}", expected),
370            found: format!("{}", inferred),
371        })
372    }
373}
374
375/// Infer the sort (universe) of a type.
376///
377/// A term is a type if its type is a Sort.
378fn infer_sort(ctx: &Context, term: &Term) -> KernelResult<Universe> {
379    let ty = infer_type(ctx, term)?;
380    match ty {
381        Term::Sort(u) => Ok(u),
382        _ => Err(KernelError::NotAType(format!("{}", term))),
383    }
384}
385
386/// Beta-reduce a term (single step, at the head).
387///
388/// (λx.body) arg → body[x := arg]
389fn beta_reduce(term: &Term) -> Term {
390    match term {
391        Term::App(func, arg) => {
392            match func.as_ref() {
393                Term::Lambda { param, body, .. } => {
394                    // Beta reduction: (λx.body) arg → body[x := arg]
395                    substitute(body, param, arg)
396                }
397                _ => term.clone(),
398            }
399        }
400        _ => term.clone(),
401    }
402}
403
404/// Compute the expected type for a match case.
405///
406/// For a constructor C : A₁ → A₂ → ... → I,
407/// the case type is: Πa₁:A₁. Πa₂:A₂. ... P(C a₁ a₂ ...)
408///
409/// For a zero-argument constructor like Zero : Nat,
410/// the case type is just P(Zero).
411///
412/// For polymorphic constructors like Nil : Π(A:Type). List A,
413/// when matching on `xs : List A`, we skip the type parameter
414/// and use the instantiated type argument instead.
415fn compute_case_type(motive: &Term, ctor_name: &str, ctor_type: &Term, disc_type: &Term) -> Term {
416    // Extract type arguments from discriminant type
417    // e.g., List A → [A], List → []
418    let type_args = extract_type_args(disc_type);
419    let num_type_args = type_args.len();
420
421    // Collect parameters from constructor type
422    let mut all_params: Vec<(String, Term)> = Vec::new();
423    let mut current = ctor_type;
424
425    while let Term::Pi {
426        param,
427        param_type,
428        body_type,
429    } = current
430    {
431        all_params.push((param.clone(), (**param_type).clone()));
432        current = body_type;
433    }
434
435    // Split into type parameters (to skip) and value parameters (to bind)
436    let (type_params, value_params): (Vec<_>, Vec<_>) = all_params
437        .into_iter()
438        .enumerate()
439        .partition(|(i, _)| *i < num_type_args);
440
441    // Generate unique names for value parameters to avoid shadowing issues
442    // Use pattern: __arg0, __arg1, etc.
443    let named_value_params: Vec<(usize, (String, Term))> = value_params
444        .into_iter()
445        .enumerate()
446        .map(|(i, (idx, (_, param_type)))| {
447            (idx, (format!("__arg{}", i), param_type))
448        })
449        .collect();
450
451    // Build C(type_args..., value_params...)
452    let mut ctor_applied = Term::Global(ctor_name.to_string());
453
454    // Apply type arguments (from discriminant type)
455    for type_arg in &type_args {
456        ctor_applied = Term::App(Box::new(ctor_applied), Box::new(type_arg.clone()));
457    }
458
459    // Apply value parameters (as bound variables with unique names)
460    for (_, (param_name, _)) in &named_value_params {
461        ctor_applied = Term::App(
462            Box::new(ctor_applied),
463            Box::new(Term::Var(param_name.clone())),
464        );
465    }
466
467    // Build P(C type_args value_params) and beta-reduce it
468    let motive_applied = Term::App(Box::new(motive.clone()), Box::new(ctor_applied));
469    let result_type = beta_reduce(&motive_applied);
470
471    // Wrap in Πa₁:A₁. Πa₂:A₂. ... for value parameters only
472    // (in reverse order to get correct nesting)
473    let mut case_type = result_type;
474    for (_, (param_name, param_type)) in named_value_params.into_iter().rev() {
475        // Substitute type arguments into parameter type
476        let mut subst_param_type = param_type;
477        for ((_, (type_param_name, _)), type_arg) in type_params.iter().zip(type_args.iter()) {
478            subst_param_type = substitute(&subst_param_type, type_param_name, type_arg);
479        }
480
481        case_type = Term::Pi {
482            param: param_name,
483            param_type: Box::new(subst_param_type),
484            body_type: Box::new(case_type),
485        };
486    }
487
488    case_type
489}
490
491/// Extract type arguments from a type application.
492///
493/// - `List A` → `[A]`
494/// - `Either A B` → `[A, B]`
495/// - `Nat` → `[]`
496fn extract_type_args(ty: &Term) -> Vec<Term> {
497    let mut args = Vec::new();
498    let mut current = ty;
499
500    while let Term::App(func, arg) = current {
501        args.push((**arg).clone());
502        current = func;
503    }
504
505    args.reverse();
506    args
507}
508
509/// Substitute a term for a variable: `body[var := replacement]`.
510///
511/// Performs capture-avoiding substitution. Variables bound by lambda,
512/// pi, or fix that shadow `var` are not substituted into.
513///
514/// # Capture Avoidance
515///
516/// Given `substitute(λx. y, "y", x)`, the result is `λx. x` (not `λx. x`
517/// with the inner x captured). This implementation relies on unique
518/// variable names from parsing.
519///
520/// # Term Forms
521///
522/// - `Sort`, `Lit`, `Hole`, `Global` - Unchanged (no variables)
523/// - `Var(name)` - Replaced if `name == var`, unchanged otherwise
524/// - `Pi`, `Lambda`, `Fix` - Substitute in components, respecting shadowing
525/// - `App`, `Match` - Substitute recursively in all subterms
526pub fn substitute(body: &Term, var: &str, replacement: &Term) -> Term {
527    match body {
528        Term::Sort(u) => Term::Sort(u.clone()),
529
530        // Literals are never substituted
531        Term::Lit(lit) => Term::Lit(lit.clone()),
532
533        // Holes are never substituted (they're implicit type placeholders)
534        Term::Hole => Term::Hole,
535
536        Term::Var(name) if name == var => replacement.clone(),
537        Term::Var(name) => Term::Var(name.clone()),
538
539        // Globals are never substituted (they're not bound variables)
540        Term::Global(name) => Term::Global(name.clone()),
541
542        Term::Pi {
543            param,
544            param_type,
545            body_type,
546        } => {
547            let new_param_type = substitute(param_type, var, replacement);
548            // Don't substitute in body if the parameter shadows var
549            let new_body_type = if param == var {
550                (**body_type).clone()
551            } else {
552                substitute(body_type, var, replacement)
553            };
554            Term::Pi {
555                param: param.clone(),
556                param_type: Box::new(new_param_type),
557                body_type: Box::new(new_body_type),
558            }
559        }
560
561        Term::Lambda {
562            param,
563            param_type,
564            body,
565        } => {
566            let new_param_type = substitute(param_type, var, replacement);
567            // Don't substitute in body if the parameter shadows var
568            let new_body = if param == var {
569                (**body).clone()
570            } else {
571                substitute(body, var, replacement)
572            };
573            Term::Lambda {
574                param: param.clone(),
575                param_type: Box::new(new_param_type),
576                body: Box::new(new_body),
577            }
578        }
579
580        Term::App(func, arg) => Term::App(
581            Box::new(substitute(func, var, replacement)),
582            Box::new(substitute(arg, var, replacement)),
583        ),
584
585        Term::Match {
586            discriminant,
587            motive,
588            cases,
589        } => Term::Match {
590            discriminant: Box::new(substitute(discriminant, var, replacement)),
591            motive: Box::new(substitute(motive, var, replacement)),
592            cases: cases
593                .iter()
594                .map(|c| substitute(c, var, replacement))
595                .collect(),
596        },
597
598        Term::Fix { name, body } => {
599            // Don't substitute in body if the fixpoint name shadows var
600            if name == var {
601                Term::Fix {
602                    name: name.clone(),
603                    body: body.clone(),
604                }
605            } else {
606                Term::Fix {
607                    name: name.clone(),
608                    body: Box::new(substitute(body, var, replacement)),
609                }
610            }
611        }
612    }
613}
614
615/// Check if type `a` is a subtype of type `b` (cumulativity).
616///
617/// Implements the subtyping relation for the Calculus of Constructions
618/// with cumulative universes.
619///
620/// # Subtyping Rules
621///
622/// - **Universe cumulativity**: `Type i <= Type j` if `i <= j`
623/// - **Pi contravariance**: `Π(x:A). B <= Π(x:A'). B'` if `A' <= A` and `B <= B'`
624/// - **Structural equality**: Other terms are compared after normalization
625///
626/// # Normalization
627///
628/// Both types are normalized before comparison, ensuring that definitionally
629/// equal types are recognized as subtypes.
630///
631/// # Cumulativity Examples
632///
633/// - `Type 0 <= Type 1` (lower universe is subtype of higher)
634/// - `Nat -> Type 0 <= Nat -> Type 1` (covariant in return type)
635/// - `Type 1 -> Nat <= Type 0 -> Nat` (contravariant in parameter type)
636pub fn is_subtype(ctx: &Context, a: &Term, b: &Term) -> bool {
637    // Normalize both terms before comparison
638    // This ensures that e.g. `ReachesOne (collatzStep 2)` equals `ReachesOne 1`
639    let a_norm = normalize(ctx, a);
640    let b_norm = normalize(ctx, b);
641
642    is_subtype_normalized(ctx, &a_norm, &b_norm)
643}
644
645/// Check subtyping on already-normalized terms.
646fn is_subtype_normalized(ctx: &Context, a: &Term, b: &Term) -> bool {
647    match (a, b) {
648        // Universe subtyping
649        (Term::Sort(u1), Term::Sort(u2)) => u1.is_subtype_of(u2),
650
651        // Pi subtyping (contravariant in param, covariant in body)
652        (
653            Term::Pi {
654                param: p1,
655                param_type: t1,
656                body_type: b1,
657            },
658            Term::Pi {
659                param: p2,
660                param_type: t2,
661                body_type: b2,
662            },
663        ) => {
664            // Contravariant: t2 ≤ t1 (the expected param can be more specific)
665            is_subtype_normalized(ctx, t2, t1) && {
666                // Covariant: b1 ≤ b2 (alpha-rename to compare bodies)
667                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
668                is_subtype_normalized(ctx, b1, &b2_renamed)
669            }
670        }
671
672        // Fall back to structural equality for other terms
673        _ => types_equal(a, b),
674    }
675}
676
677/// Extract the inductive type name from a type.
678///
679/// Handles both:
680/// - Simple inductives: `Nat` → Some("Nat")
681/// - Polymorphic inductives: `List A` → Some("List")
682///
683/// Returns None if the type is not an inductive type.
684fn extract_inductive_name(ctx: &Context, ty: &Term) -> Option<String> {
685    match ty {
686        // Simple case: Global("Nat")
687        Term::Global(name) if ctx.is_inductive(name) => Some(name.clone()),
688
689        // Polymorphic case: App(App(...App(Global("List"), _)...), _)
690        // Recursively unwrap App to find the base Global
691        Term::App(func, _) => extract_inductive_name(ctx, func),
692
693        _ => None,
694    }
695}
696
697/// Check if two types are equal (up to alpha-equivalence).
698///
699/// Two terms are alpha-equivalent if they are the same up to
700/// renaming of bound variables.
701fn types_equal(a: &Term, b: &Term) -> bool {
702    // Hole matches anything (it's a type wildcard)
703    if matches!(a, Term::Hole) || matches!(b, Term::Hole) {
704        return true;
705    }
706
707    match (a, b) {
708        (Term::Sort(u1), Term::Sort(u2)) => u1 == u2,
709
710        (Term::Lit(l1), Term::Lit(l2)) => l1 == l2,
711
712        (Term::Var(n1), Term::Var(n2)) => n1 == n2,
713
714        (Term::Global(n1), Term::Global(n2)) => n1 == n2,
715
716        (
717            Term::Pi {
718                param: p1,
719                param_type: t1,
720                body_type: b1,
721            },
722            Term::Pi {
723                param: p2,
724                param_type: t2,
725                body_type: b2,
726            },
727        ) => {
728            types_equal(t1, t2) && {
729                // Alpha-equivalence: rename p2 to p1 in b2
730                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
731                types_equal(b1, &b2_renamed)
732            }
733        }
734
735        (
736            Term::Lambda {
737                param: p1,
738                param_type: t1,
739                body: b1,
740            },
741            Term::Lambda {
742                param: p2,
743                param_type: t2,
744                body: b2,
745            },
746        ) => {
747            types_equal(t1, t2) && {
748                let b2_renamed = substitute(b2, p2, &Term::Var(p1.clone()));
749                types_equal(b1, &b2_renamed)
750            }
751        }
752
753        (Term::App(f1, a1), Term::App(f2, a2)) => types_equal(f1, f2) && types_equal(a1, a2),
754
755        (
756            Term::Match {
757                discriminant: d1,
758                motive: m1,
759                cases: c1,
760            },
761            Term::Match {
762                discriminant: d2,
763                motive: m2,
764                cases: c2,
765            },
766        ) => {
767            types_equal(d1, d2)
768                && types_equal(m1, m2)
769                && c1.len() == c2.len()
770                && c1.iter().zip(c2.iter()).all(|(a, b)| types_equal(a, b))
771        }
772
773        (
774            Term::Fix {
775                name: n1,
776                body: b1,
777            },
778            Term::Fix {
779                name: n2,
780                body: b2,
781            },
782        ) => {
783            // Alpha-equivalence: rename n2 to n1 in b2
784            let b2_renamed = substitute(b2, n2, &Term::Var(n1.clone()));
785            types_equal(b1, &b2_renamed)
786        }
787
788        _ => false,
789    }
790}