Skip to main content

qala_compiler/
types.rs

1//! the type lattice the type checker resolves AST type expressions and
2//! inferred local types into.
3//!
4//! one enum, [`QalaType`], covers every type the language can talk about: the
5//! six primitives, fixed and dynamic arrays, tuples, function types, user
6//! `Named` types (`struct` / `enum` / `interface`), the built-in `Result` and
7//! `Option`, the opaque `FileHandle` returned by `open`, and the sentinel
8//! `Unknown` poison type. equality is structural on primitives and compounds,
9//! nominal on `Named` (via [`Symbol`]); `Unknown` is symmetrically equal to
10//! anything under [`QalaType::types_match`], which is what suppresses
11//! cascading errors after the first type failure.
12//!
13//! this file depends only on [`crate::ast::PrimType`] (to bridge from the
14//! parser's primitive-type tokens to a [`QalaType`] variant). there are no
15//! diagnostics, no symbol-table lookups, and no inference logic here -- those
16//! belong in later modules. keeping the dependency surface small keeps the
17//! type lattice reusable and easy to test.
18
19use crate::ast::PrimType;
20
21/// an interned name: the string that a [`QalaType::Named`] carries when it
22/// stands for a user-declared struct, enum, or interface.
23///
24/// a thin newtype around [`String`] rather than a bare `String` -- it
25/// documents intent at every call site (this is a *name*, not arbitrary text)
26/// and prevents accidental mixing with display strings.
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct Symbol(pub String);
29
30/// every type the type checker can talk about.
31///
32/// derive `Debug, Clone, PartialEq` only -- no `Eq`, because future variants
33/// may carry float-valued metadata, and no `serde`, because the typed AST and
34/// codegen run in-process and never serialize a type. structural equality on
35/// primitives and compounds, nominal equality on [`Named`](Self::Named). use
36/// [`QalaType::types_match`] (not bare `==`) for the type-equality predicate
37/// the type checker actually consults -- it adds the [`Unknown`](Self::Unknown)
38/// poison-as-wildcard rule that `==` does not.
39#[derive(Debug, Clone, PartialEq)]
40pub enum QalaType {
41    /// the 64-bit signed integer primitive.
42    I64,
43    /// the 64-bit IEEE 754 float primitive.
44    F64,
45    /// the boolean primitive.
46    Bool,
47    /// the string primitive.
48    Str,
49    /// the byte primitive (one 8-bit unsigned value, distinct from `i64`).
50    Byte,
51    /// the empty type, used as the return type of a function that has no
52    /// declared `-> T` and as the value type of statement-shaped expressions.
53    Void,
54    /// an array. `Some(n)` is the fixed-length `[T; n]` form; `None` is the
55    /// dynamic `[T]` form. the element type is boxed because an array of
56    /// arrays is legal and the box keeps [`QalaType`] a fixed size.
57    Array(Box<QalaType>, Option<usize>),
58    /// a tuple: an ordered list of element types. an empty tuple is the
59    /// `void`-shaped tuple that the language does not write directly (a
60    /// trailing-comma single-element tuple is a [`QalaType::Tuple`] of one).
61    Tuple(Vec<QalaType>),
62    /// a function type. `params` is the (un-named) parameter types in order;
63    /// `returns` is boxed for the same fixed-size reason as `Array`.
64    Function {
65        /// the parameter types in declaration order.
66        params: Vec<QalaType>,
67        /// the return type.
68        returns: Box<QalaType>,
69    },
70    /// a user-declared type referenced by name -- `struct` / `enum` /
71    /// `interface`. nominal equality: two `Named` values are equal iff their
72    /// [`Symbol`]s are equal.
73    Named(Symbol),
74    /// the built-in `Result<T, E>`. the type checker resolves a generic
75    /// `Result<T, E>` type expression to this variant directly, without a
76    /// user-defined generic mechanism.
77    Result(Box<QalaType>, Box<QalaType>),
78    /// the built-in `Option<T>`. the type checker resolves a generic
79    /// `Option<T>` type expression to this variant directly.
80    Option(Box<QalaType>),
81    /// the opaque handle returned by the stdlib `open(path)` call. a built-in
82    /// named type rather than a `Named(Symbol("FileHandle"))` so users do not
83    /// need to declare it; the stdlib signature table refers to this variant
84    /// by name. closing a `FileHandle` (`close(h)`) is checked structurally
85    /// the same way as any other call.
86    FileHandle,
87    /// the poison type emitted on a type error so the rest of the program
88    /// keeps typing. equal to anything under [`QalaType::types_match`] -- the
89    /// research's Pattern 3 -- so a chain of expressions after the first error
90    /// does not cascade into a wall of follow-on errors.
91    Unknown,
92}
93
94impl QalaType {
95    /// the type-equality predicate the type checker uses everywhere.
96    ///
97    /// structural on primitives, [`Array`](Self::Array), [`Tuple`](Self::Tuple),
98    /// [`Function`](Self::Function), [`Result`](Self::Result), and
99    /// [`Option`](Self::Option); nominal on [`Named`](Self::Named) (matches by
100    /// [`Symbol`] equality); reflexive on [`FileHandle`](Self::FileHandle).
101    /// [`Unknown`](Self::Unknown) is symmetrically equal to anything --
102    /// `Unknown.types_match(x)` and `x.types_match(Unknown)` are both `true`
103    /// for every `x`. that rule is what stops one type error from cascading
104    /// into many; it is Pattern 3 in the phase research.
105    ///
106    /// note this is NOT the same as `==`. `==` is derived structural equality
107    /// across the whole enum; it does not treat `Unknown` as a wildcard. always
108    /// use `types_match` when asking "do these two types agree, for the
109    /// purpose of type-checking?".
110    pub fn types_match(&self, other: &QalaType) -> bool {
111        use QalaType::*;
112        match (self, other) {
113            // poison is symmetrically equal to anything.
114            (Unknown, _) | (_, Unknown) => true,
115            // primitives compare by tag only.
116            (I64, I64)
117            | (F64, F64)
118            | (Bool, Bool)
119            | (Str, Str)
120            | (Byte, Byte)
121            | (Void, Void)
122            | (FileHandle, FileHandle) => true,
123            // arrays compare by element AND length kind (Some(n) only matches
124            // the same Some(n); None only matches None).
125            (Array(a, asize), Array(b, bsize)) => asize == bsize && a.types_match(b),
126            // tuples compare element-wise, including length.
127            (Tuple(a), Tuple(b)) => {
128                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.types_match(y))
129            }
130            // function types compare param count, param types pairwise, return.
131            (
132                Function {
133                    params: ap,
134                    returns: ar,
135                },
136                Function {
137                    params: bp,
138                    returns: br,
139                },
140            ) => {
141                ap.len() == bp.len()
142                    && ap.iter().zip(bp.iter()).all(|(x, y)| x.types_match(y))
143                    && ar.types_match(br)
144            }
145            // Named is nominal -- match iff the carried symbols are equal.
146            (Named(a), Named(b)) => a == b,
147            // Result and Option compare their type arguments structurally.
148            (Result(a_ok, a_err), Result(b_ok, b_err)) => {
149                a_ok.types_match(b_ok) && a_err.types_match(b_err)
150            }
151            (Option(a), Option(b)) => a.types_match(b),
152            // anything else is a mismatch.
153            _ => false,
154        }
155    }
156
157    /// the canonical lowercase form used in `expected X, found Y` wording.
158    ///
159    /// `i64`, `f64`, `bool`, `str`, `byte`, `void`, `[i64; 5]` (fixed array),
160    /// `[i64]` (dynamic array), `(i64, bool)` (tuple), `fn(i64) -> i64`
161    /// (function), `Shape` (named -- the symbol's text verbatim),
162    /// `Result<i64, str>`, `Option<i64>`, `FileHandle`, and `?` for
163    /// [`Unknown`](Self::Unknown). the question-mark form for `Unknown` keeps
164    /// error messages short when poisoning propagates: "expected i64, found ?"
165    /// reads better than "expected i64, found unknown".
166    ///
167    /// recursive: an `Array(Box<I64>, Some(5))` renders `"[i64; 5]"`; a
168    /// `Function { params: vec![I64, Bool], returns: Box::new(Str) }`
169    /// renders `"fn(i64, bool) -> str"`.
170    pub fn display(&self) -> String {
171        use QalaType::*;
172        match self {
173            I64 => "i64".to_string(),
174            F64 => "f64".to_string(),
175            Bool => "bool".to_string(),
176            Str => "str".to_string(),
177            Byte => "byte".to_string(),
178            Void => "void".to_string(),
179            Array(elem, Some(n)) => format!("[{}; {}]", elem.display(), n),
180            Array(elem, None) => format!("[{}]", elem.display()),
181            Tuple(elems) => {
182                let inner: Vec<String> = elems.iter().map(|t| t.display()).collect();
183                format!("({})", inner.join(", "))
184            }
185            Function { params, returns } => {
186                let inner: Vec<String> = params.iter().map(|t| t.display()).collect();
187                format!("fn({}) -> {}", inner.join(", "), returns.display())
188            }
189            Named(Symbol(name)) => name.clone(),
190            Result(ok, err) => format!("Result<{}, {}>", ok.display(), err.display()),
191            Option(inner) => format!("Option<{}>", inner.display()),
192            FileHandle => "FileHandle".to_string(),
193            Unknown => "?".to_string(),
194        }
195    }
196
197    /// bridge from the parser's [`PrimType`] (which records *which keyword
198    /// appeared*) to the corresponding [`QalaType`] primitive variant.
199    ///
200    /// the type checker's `TypeExpr` resolver calls this for the primitive
201    /// case; the compound cases (`Array` / `DynArray` / `Tuple` / `Fn` /
202    /// `Generic` / `Named`) recurse into the resolver instead.
203    pub fn from_prim_type(p: &PrimType) -> Self {
204        match p {
205            PrimType::I64 => QalaType::I64,
206            PrimType::F64 => QalaType::F64,
207            PrimType::Bool => QalaType::Bool,
208            PrimType::Str => QalaType::Str,
209            PrimType::Byte => QalaType::Byte,
210            PrimType::Void => QalaType::Void,
211        }
212    }
213}
214
215impl core::fmt::Display for QalaType {
216    /// delegate to [`QalaType::display`] so a `format!("{ty}")` produces the
217    /// canonical form. lets error messages interpolate types without an
218    /// explicit `.display()` call at every site.
219    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
220        f.write_str(&self.display())
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    /// a short helper so test rows stay readable.
229    fn ty_match(a: &QalaType, b: &QalaType) -> bool {
230        a.types_match(b)
231    }
232
233    /// boxed convenience so test rows do not repeat `Box::new` everywhere.
234    fn b(t: QalaType) -> Box<QalaType> {
235        Box::new(t)
236    }
237
238    #[test]
239    fn every_primitive_equals_itself() {
240        let prims = [
241            QalaType::I64,
242            QalaType::F64,
243            QalaType::Bool,
244            QalaType::Str,
245            QalaType::Byte,
246            QalaType::Void,
247            QalaType::FileHandle,
248        ];
249        for p in &prims {
250            assert!(ty_match(p, p), "{p:?} should match itself");
251            assert_eq!(p.clone(), p.clone(), "{p:?} should be == itself");
252        }
253    }
254
255    #[test]
256    fn distinct_primitives_are_not_equal() {
257        // a representative non-equal pair from every position.
258        assert!(!ty_match(&QalaType::I64, &QalaType::F64));
259        assert!(!ty_match(&QalaType::Bool, &QalaType::Str));
260        assert!(!ty_match(&QalaType::Byte, &QalaType::I64));
261        assert!(!ty_match(&QalaType::Void, &QalaType::Bool));
262        assert!(!ty_match(&QalaType::FileHandle, &QalaType::Str));
263    }
264
265    #[test]
266    fn arrays_compare_element_and_length_kind() {
267        let fixed5 = QalaType::Array(b(QalaType::I64), Some(5));
268        let fixed5_again = QalaType::Array(b(QalaType::I64), Some(5));
269        let fixed6 = QalaType::Array(b(QalaType::I64), Some(6));
270        let dynamic = QalaType::Array(b(QalaType::I64), None);
271        assert!(ty_match(&fixed5, &fixed5_again));
272        assert!(!ty_match(&fixed5, &fixed6));
273        assert!(!ty_match(&fixed5, &dynamic));
274        assert!(!ty_match(&dynamic, &fixed6));
275        // a different element type does not match either length kind.
276        let dynamic_bool = QalaType::Array(b(QalaType::Bool), None);
277        assert!(!ty_match(&dynamic, &dynamic_bool));
278    }
279
280    #[test]
281    fn tuples_compare_elementwise_with_order_significant() {
282        let i64_bool = QalaType::Tuple(vec![QalaType::I64, QalaType::Bool]);
283        let i64_bool_again = QalaType::Tuple(vec![QalaType::I64, QalaType::Bool]);
284        let bool_i64 = QalaType::Tuple(vec![QalaType::Bool, QalaType::I64]);
285        let single = QalaType::Tuple(vec![QalaType::I64]);
286        assert!(ty_match(&i64_bool, &i64_bool_again));
287        // order matters.
288        assert!(!ty_match(&i64_bool, &bool_i64));
289        // length matters.
290        assert!(!ty_match(&i64_bool, &single));
291    }
292
293    #[test]
294    fn function_types_compare_params_in_order_and_return() {
295        let f_i64_to_i64 = QalaType::Function {
296            params: vec![QalaType::I64],
297            returns: b(QalaType::I64),
298        };
299        let f_i64_to_i64_again = QalaType::Function {
300            params: vec![QalaType::I64],
301            returns: b(QalaType::I64),
302        };
303        let f_str_to_i64 = QalaType::Function {
304            params: vec![QalaType::Str],
305            returns: b(QalaType::I64),
306        };
307        let f_i64_to_str = QalaType::Function {
308            params: vec![QalaType::I64],
309            returns: b(QalaType::Str),
310        };
311        let f_i64_i64_to_i64 = QalaType::Function {
312            params: vec![QalaType::I64, QalaType::I64],
313            returns: b(QalaType::I64),
314        };
315        assert!(ty_match(&f_i64_to_i64, &f_i64_to_i64_again));
316        // param-type mismatch.
317        assert!(!ty_match(&f_i64_to_i64, &f_str_to_i64));
318        // return-type mismatch.
319        assert!(!ty_match(&f_i64_to_i64, &f_i64_to_str));
320        // param-count mismatch.
321        assert!(!ty_match(&f_i64_to_i64, &f_i64_i64_to_i64));
322    }
323
324    #[test]
325    fn named_types_compare_nominally_by_symbol() {
326        let shape = QalaType::Named(Symbol("Shape".to_string()));
327        let shape_again = QalaType::Named(Symbol("Shape".to_string()));
328        let point = QalaType::Named(Symbol("Point".to_string()));
329        assert!(ty_match(&shape, &shape_again));
330        assert!(!ty_match(&shape, &point));
331        // nominal equality means structurally-identical-but-named-differently
332        // types are still distinct.
333        assert!(!ty_match(
334            &shape,
335            &QalaType::Named(Symbol("shape".to_string()))
336        ));
337    }
338
339    #[test]
340    fn result_and_option_compare_their_arguments_structurally() {
341        let r_i64_str = QalaType::Result(b(QalaType::I64), b(QalaType::Str));
342        let r_i64_str_again = QalaType::Result(b(QalaType::I64), b(QalaType::Str));
343        let r_i64_bool = QalaType::Result(b(QalaType::I64), b(QalaType::Bool));
344        let r_str_str = QalaType::Result(b(QalaType::Str), b(QalaType::Str));
345        let o_i64 = QalaType::Option(b(QalaType::I64));
346        assert!(ty_match(&r_i64_str, &r_i64_str_again));
347        // error type differs.
348        assert!(!ty_match(&r_i64_str, &r_i64_bool));
349        // ok type differs.
350        assert!(!ty_match(&r_i64_str, &r_str_str));
351        // a Result is distinct from an Option even with matching ok type.
352        assert!(!ty_match(&r_i64_str, &o_i64));
353    }
354
355    #[test]
356    fn unknown_is_symmetrically_equal_to_anything() {
357        // the poison-propagation rule from research Pattern 3.
358        let samples = [
359            QalaType::I64,
360            QalaType::Str,
361            QalaType::Array(b(QalaType::I64), None),
362            QalaType::Named(Symbol("X".to_string())),
363            QalaType::Result(b(QalaType::I64), b(QalaType::Str)),
364            QalaType::Unknown,
365        ];
366        for s in &samples {
367            // Unknown on the left matches anything.
368            assert!(
369                ty_match(&QalaType::Unknown, s),
370                "Unknown should match {s:?}"
371            );
372            // Unknown on the right matches anything.
373            assert!(
374                ty_match(s, &QalaType::Unknown),
375                "{s:?} should match Unknown"
376            );
377        }
378        // Unknown matches Unknown -- explicit so a regression here is loud.
379        assert!(ty_match(&QalaType::Unknown, &QalaType::Unknown));
380    }
381
382    #[test]
383    fn types_match_is_symmetric_and_reflexive_on_concrete_pairs() {
384        // a small sample table covering primitives, an array, a tuple, a
385        // function, a named, a Result, and an Option. for every (a, b) the
386        // predicate must give the same answer regardless of argument order;
387        // for every (a, a) it must say "true".
388        let samples: Vec<QalaType> = vec![
389            QalaType::I64,
390            QalaType::F64,
391            QalaType::Bool,
392            QalaType::Str,
393            QalaType::Byte,
394            QalaType::Void,
395            QalaType::FileHandle,
396            QalaType::Array(b(QalaType::I64), Some(3)),
397            QalaType::Array(b(QalaType::I64), None),
398            QalaType::Tuple(vec![QalaType::I64, QalaType::Bool]),
399            QalaType::Function {
400                params: vec![QalaType::I64],
401                returns: b(QalaType::Bool),
402            },
403            QalaType::Named(Symbol("Shape".to_string())),
404            QalaType::Result(b(QalaType::I64), b(QalaType::Str)),
405            QalaType::Option(b(QalaType::I64)),
406        ];
407        for a in &samples {
408            // reflexive.
409            assert!(ty_match(a, a), "reflexive failure on {a:?}");
410            for c in &samples {
411                // symmetric.
412                assert_eq!(
413                    ty_match(a, c),
414                    ty_match(c, a),
415                    "symmetry failure on ({a:?}, {c:?})",
416                );
417            }
418        }
419    }
420
421    #[test]
422    fn display_produces_the_canonical_lowercase_form() {
423        // the wording the TypeMismatch message and the renderer expect.
424        assert_eq!(QalaType::I64.display(), "i64");
425        assert_eq!(QalaType::F64.display(), "f64");
426        assert_eq!(QalaType::Bool.display(), "bool");
427        assert_eq!(QalaType::Str.display(), "str");
428        assert_eq!(QalaType::Byte.display(), "byte");
429        assert_eq!(QalaType::Void.display(), "void");
430        assert_eq!(QalaType::FileHandle.display(), "FileHandle");
431        assert_eq!(
432            QalaType::Array(b(QalaType::I64), Some(5)).display(),
433            "[i64; 5]",
434        );
435        assert_eq!(QalaType::Array(b(QalaType::I64), None).display(), "[i64]");
436        assert_eq!(
437            QalaType::Tuple(vec![QalaType::I64, QalaType::Bool]).display(),
438            "(i64, bool)",
439        );
440        assert_eq!(
441            QalaType::Function {
442                params: vec![QalaType::I64],
443                returns: b(QalaType::I64)
444            }
445            .display(),
446            "fn(i64) -> i64",
447        );
448        assert_eq!(
449            QalaType::Function {
450                params: vec![QalaType::I64, QalaType::Bool],
451                returns: b(QalaType::Str),
452            }
453            .display(),
454            "fn(i64, bool) -> str",
455        );
456        assert_eq!(
457            QalaType::Named(Symbol("Shape".to_string())).display(),
458            "Shape"
459        );
460        assert_eq!(
461            QalaType::Result(b(QalaType::I64), b(QalaType::Str)).display(),
462            "Result<i64, str>",
463        );
464        assert_eq!(QalaType::Option(b(QalaType::I64)).display(), "Option<i64>");
465        assert_eq!(QalaType::Unknown.display(), "?");
466        // the `Display` impl matches `.display()`.
467        assert_eq!(format!("{}", QalaType::I64), "i64");
468        assert_eq!(format!("{}", QalaType::Unknown), "?");
469    }
470
471    #[test]
472    fn from_prim_type_bridges_every_prim_type_variant() {
473        assert!(ty_match(
474            &QalaType::from_prim_type(&PrimType::I64),
475            &QalaType::I64
476        ));
477        assert!(ty_match(
478            &QalaType::from_prim_type(&PrimType::F64),
479            &QalaType::F64
480        ));
481        assert!(ty_match(
482            &QalaType::from_prim_type(&PrimType::Bool),
483            &QalaType::Bool
484        ));
485        assert!(ty_match(
486            &QalaType::from_prim_type(&PrimType::Str),
487            &QalaType::Str
488        ));
489        assert!(ty_match(
490            &QalaType::from_prim_type(&PrimType::Byte),
491            &QalaType::Byte
492        ));
493        assert!(ty_match(
494            &QalaType::from_prim_type(&PrimType::Void),
495            &QalaType::Void
496        ));
497    }
498}