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, ¶m_type)?;
118
119 // Substitute argument into body type
120 Ok(substitute(&body_type, ¶m, 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}