aver/ast/types.rs
1//! Aver static type representation.
2//!
3//! Lives under `crate::ast` so that `Spanned<T>` can carry an optional
4//! `Type` annotation without forming a cycle through `crate::types`
5//! (which depends on `crate::ast`). The original `crate::types::Type`
6//! is re-exported from here for backward compatibility.
7
8use crate::ir::TypeId;
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum Type {
12 Int,
13 Float,
14 Str,
15 Bool,
16 Unit,
17 Result(Box<Type>, Box<Type>),
18 Option(Box<Type>),
19 List(Box<Type>),
20 Tuple(Vec<Type>),
21 Map(Box<Type>, Box<Type>),
22 Vector(Box<Type>),
23 Fn(Vec<Type>, Box<Type>, Vec<String>),
24 Var(String), // named type variable in polymorphic builtin signatures (instantiated at call site)
25 Invalid, // checker recovery after an earlier error; matches anything in `.compatible` to suppress cascading diagnostics
26 /// User-defined or builtin named type. The `id` is `Some` once the
27 /// typechecker has resolved the reference against the program's
28 /// [`crate::ir::SymbolTable`] (#138 phase B). The `id` is `None` for
29 /// transient parser output (before typecheck has run), for builtin
30 /// record types (`HttpResponse`, `Header`, `Tcp.Connection`,
31 /// `Buffer`, …) that aren't registered in the user-program symbol
32 /// table, and for stamps the checker couldn't resolve.
33 ///
34 /// Identity:
35 /// - two `Named` with both `id = Some` compare by `id` (typed
36 /// identity — cross-module same-bare-name types stay distinct);
37 /// - otherwise fall back to source-faithful name comparison, with
38 /// the historical suffix tolerance for `Bare` vs `Module.Bare`.
39 Named {
40 id: Option<TypeId>,
41 name: String,
42 },
43}
44
45impl Type {
46 /// Build a `Type::Named` whose identity has not been resolved
47 /// against a `SymbolTable` (parser output, builtins, in-flight
48 /// stamps). Equivalent to `Named { id: None, name: name.into() }`.
49 pub fn named(name: impl Into<String>) -> Self {
50 Self::Named {
51 id: None,
52 name: name.into(),
53 }
54 }
55
56 /// Build a `Type::Named` resolved against the program's
57 /// `SymbolTable`. Caller is responsible for ensuring `name` is the
58 /// canonical form (`symbol_table.type_entry(id).key.canonical()`).
59 pub fn named_resolved(id: TypeId, name: impl Into<String>) -> Self {
60 Self::Named {
61 id: Some(id),
62 name: name.into(),
63 }
64 }
65
66 /// Source-faithful name of a `Named` (`"Shape"` / `"A.Shape"` /
67 /// `"HttpResponse"`); `None` for any non-`Named` variant.
68 pub fn named_name(&self) -> Option<&str> {
69 match self {
70 Type::Named { name, .. } => Some(name.as_str()),
71 _ => None,
72 }
73 }
74
75 /// Resolved `TypeId` of a `Named`, if any. `None` for unresolved
76 /// references and for any non-`Named` variant.
77 pub fn named_id(&self) -> Option<TypeId> {
78 match self {
79 Type::Named { id, .. } => *id,
80 _ => None,
81 }
82 }
83
84 /// `a.compatible(b)` — can a value of type `self` be used where `other` is expected?
85 /// Two concrete types must be equal (structurally) to be compatible. Type variables
86 /// are resolved by the type checker at call sites, not by this raw relation.
87 ///
88 /// Iron — A4: `Type::Invalid` is the checker's "already-errored"
89 /// sentinel and matches anything. Without this, a single bad
90 /// expression would fan its `Invalid` type out through every
91 /// downstream `.compatible(...)` check and produce a chain of
92 /// duplicate diagnostics.
93 pub fn compatible(&self, other: &Type) -> bool {
94 match (self, other) {
95 (Type::Invalid, _) | (_, Type::Invalid) => true,
96 (Type::Int, Type::Int) => true,
97 (Type::Float, Type::Float) => true,
98 (Type::Str, Type::Str) => true,
99 (Type::Bool, Type::Bool) => true,
100 (Type::Unit, Type::Unit) => true,
101 (Type::Var(a), Type::Var(b)) => a == b,
102 (Type::Result(a1, b1), Type::Result(a2, b2)) => a1.compatible(a2) && b1.compatible(b2),
103 (Type::Option(a), Type::Option(b)) => a.compatible(b),
104 (Type::List(a), Type::List(b)) => a.compatible(b),
105 (Type::Tuple(a), Type::Tuple(b)) => {
106 a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.compatible(y))
107 }
108 (Type::Map(k1, v1), Type::Map(k2, v2)) => k1.compatible(k2) && v1.compatible(v2),
109 (Type::Vector(a), Type::Vector(b)) => a.compatible(b),
110 (Type::Fn(p1, r1, e1), Type::Fn(p2, r2, e2)) => {
111 p1.len() == p2.len()
112 && p1.iter().zip(p2.iter()).all(|(a, b)| a.compatible(b))
113 && r1.compatible(r2)
114 && e1.iter().all(|actual| {
115 e2.iter()
116 .any(|expected| crate::effects::effect_satisfies(expected, actual))
117 })
118 }
119 (
120 Type::Named {
121 id: id_a,
122 name: name_a,
123 },
124 Type::Named {
125 id: id_b,
126 name: name_b,
127 },
128 ) => match (id_a, id_b) {
129 // Both sides resolved against the symbol table: typed
130 // identity is the load-bearing comparison. Two distinct
131 // `TypeId`s never compare equal even when their source
132 // names happen to coincide (cross-module `Shape` —
133 // distinct `TypeId`, must stay incompatible).
134 (Some(a), Some(b)) => a == b,
135 // Exactly one side carries a `TypeId`: the typechecker
136 // already classified that side; the other side
137 // attempted resolution and came up empty. Always
138 // reject — silent name fallback here was the round-6
139 // entry-fallback bug, where a dep module's
140 // unresolved bare `Shape` silently bound to the
141 // entry module's own `Shape`. Builtins like
142 // `HttpResponse` never set `id` on either side, so
143 // they exercise the `(None, None)` branch below;
144 // genuine cross-module typed/raw mixes hit this
145 // branch and must fail.
146 (Some(_), None) | (None, Some(_)) => false,
147 // Both sides unresolved (raw stamps, builtin records,
148 // tests). Keep the historical suffix relation as a
149 // best-effort match — there's no typed identity to
150 // disagree with on either side.
151 (None, None) => {
152 name_a == name_b
153 || name_a.ends_with(&format!(".{}", name_b))
154 || name_b.ends_with(&format!(".{}", name_a))
155 }
156 },
157 _ => false,
158 }
159 }
160
161 pub fn display(&self) -> String {
162 match self {
163 Type::Int => "Int".to_string(),
164 Type::Float => "Float".to_string(),
165 Type::Str => "String".to_string(),
166 Type::Bool => "Bool".to_string(),
167 Type::Unit => "Unit".to_string(),
168 Type::Result(ok, err) => format!("Result<{}, {}>", ok.display(), err.display()),
169 Type::Option(inner) => format!("Option<{}>", inner.display()),
170 Type::List(inner) => format!("List<{}>", inner.display()),
171 Type::Tuple(items) => format!(
172 "Tuple<{}>",
173 items
174 .iter()
175 .map(Type::display)
176 .collect::<Vec<_>>()
177 .join(", ")
178 ),
179 Type::Map(key, value) => format!("Map<{}, {}>", key.display(), value.display()),
180 Type::Vector(inner) => format!("Vector<{}>", inner.display()),
181 Type::Fn(params, ret, effects) => {
182 let ps: Vec<String> = params.iter().map(|p| p.display()).collect();
183 if effects.is_empty() {
184 format!("Fn({}) -> {}", ps.join(", "), ret.display())
185 } else {
186 format!(
187 "Fn({}) -> {} ! [{}]",
188 ps.join(", "),
189 ret.display(),
190 effects.join(", ")
191 )
192 }
193 }
194 Type::Var(name) => name.clone(),
195 Type::Invalid => "Invalid".to_string(),
196 Type::Named { name, .. } => name.clone(),
197 }
198 }
199}