Skip to main content

type_lang/
error.rs

1//! The error returned when two types cannot be unified.
2
3use core::fmt;
4
5use crate::ty::{TyVar, Type};
6
7/// The reason a [`unify`](crate::Unifier::unify) call failed.
8///
9/// Unification fails in exactly two ways, each a distinct, defined outcome rather
10/// than a panic:
11///
12/// - the two types are built from different constructors, or the same constructor
13///   applied to a different number of arguments ([`Mismatch`](Self::Mismatch)), or
14/// - binding a variable would make it refer to itself, producing an infinite type
15///   ([`Occurs`](Self::Occurs)).
16///
17/// Both variants carry the offending types, fully resolved under the substitution
18/// at the point of failure, so the failure is actionable when it is rendered far
19/// from the call that produced it — typically by mapping it onto a `diag-lang`
20/// diagnostic with the consumer's own type names.
21///
22/// The enum is `#[non_exhaustive]`: a downstream `match` must include a wildcard
23/// arm, so a later addition is a minor change, not a breaking one.
24///
25/// # Examples
26///
27/// ```
28/// use type_lang::{TyCon, Type, TypeError, Unifier};
29///
30/// const INT: TyCon = TyCon::new(0);
31/// const BOOL: TyCon = TyCon::new(1);
32///
33/// let mut unifier = Unifier::new();
34/// let err = unifier
35///     .unify(&Type::con(INT), &Type::con(BOOL))
36///     .unwrap_err();
37/// assert!(matches!(err, TypeError::Mismatch { .. }));
38/// ```
39#[derive(Clone, Debug, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum TypeError {
42    /// The two types do not share a constructor.
43    ///
44    /// Returned when the heads differ (`Int` versus `Bool`) or when the same head
45    /// is applied to a different arity (`Pair<A>` versus `Pair<A, B>`). Both sides
46    /// are resolved under the current substitution before being stored, so they
47    /// show the most concrete form known at the point of failure.
48    ///
49    /// `unify(a, b)` records `a` as `expected` and `b` as `found`. Unification
50    /// itself is symmetric — the labels only reflect the argument order, to match
51    /// the common "expected this, found that" phrasing of a type error.
52    Mismatch {
53        /// The first type given to `unify`, resolved.
54        expected: Type,
55        /// The second type given to `unify`, resolved.
56        found: Type,
57    },
58
59    /// Binding a variable would make it occur within its own definition.
60    ///
61    /// Returned when a variable would be bound to a type that already contains it —
62    /// for example unifying `?0` with `List<?0>`. Such a binding describes an
63    /// infinitely deep type, which the occurs check rejects so that resolution
64    /// always terminates.
65    Occurs {
66        /// The variable that would refer to itself.
67        var: TyVar,
68        /// The type it would have been bound to, resolved.
69        ty: Type,
70    },
71}
72
73impl fmt::Display for TypeError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Mismatch { expected, found } => {
77                write!(f, "type mismatch: expected `{expected}`, found `{found}`")
78            }
79            Self::Occurs { var, ty } => {
80                write!(
81                    f,
82                    "recursive type: variable `?{}` occurs in `{ty}`",
83                    var.to_u32(),
84                )
85            }
86        }
87    }
88}
89
90impl core::error::Error for TypeError {}
91
92#[cfg(test)]
93mod tests {
94    extern crate alloc;
95    use alloc::string::ToString;
96    use alloc::vec;
97
98    use super::*;
99    use crate::ty::TyCon;
100
101    const INT: TyCon = TyCon::new(0);
102    const LIST: TyCon = TyCon::new(7);
103
104    #[test]
105    fn test_mismatch_display_names_both_types() {
106        let err = TypeError::Mismatch {
107            expected: Type::con(INT),
108            found: Type::app(LIST, vec![Type::con(INT)]),
109        };
110        let text = err.to_string();
111        assert!(text.contains("#0"), "{text}");
112        assert!(text.contains("#7(#0)"), "{text}");
113    }
114
115    #[test]
116    fn test_occurs_display_names_variable() {
117        let err = TypeError::Occurs {
118            var: TyVar::from_index(3),
119            ty: Type::app(LIST, vec![Type::var(TyVar::from_index(3))]),
120        };
121        let text = err.to_string();
122        assert!(text.contains("?3"), "{text}");
123    }
124
125    #[test]
126    fn test_error_is_clonable_and_equatable() {
127        let a = TypeError::Mismatch {
128            expected: Type::con(INT),
129            found: Type::con(LIST),
130        };
131        let b = a.clone();
132        assert_eq!(a, b);
133    }
134}