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}