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 }
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}