Skip to main content

aver/types/checker/
mod.rs

1/// Aver static type checker.
2///
3/// Two-phase analysis:
4///   Phase 1 — build a signature table from all FnDef nodes and builtins.
5///   Phase 2 — check top-level statements, then each FnDef for call-site
6///              argument types, return type, BinOp compatibility, and effects.
7///
8/// The checker keeps gradual typing for nested placeholders, but applies
9/// stricter rules for checker constraints: a bare `Unknown` does not satisfy
10/// a concrete expected type in argument/return/ascription checks.
11use std::collections::{HashMap, HashSet};
12
13use super::{Type, parse_type_str_strict};
14use crate::ast::{
15    BinOp, Expr, FnDef, Literal, Module, Pattern, Spanned, Stmt, TailCallData, TopLevel, TypeDef,
16};
17
18mod builtins;
19mod exhaustiveness;
20mod flow;
21mod infer;
22mod memo;
23mod modules;
24
25#[cfg(test)]
26mod tests;
27
28// ---------------------------------------------------------------------------
29// Public API
30// ---------------------------------------------------------------------------
31
32#[derive(Debug, Clone)]
33pub struct TypeError {
34    pub message: String,
35    pub line: usize,
36    pub col: usize,
37    /// Optional secondary span for multi-region diagnostics (e.g. declared type vs actual return).
38    pub secondary: Option<TypeErrorSpan>,
39}
40
41#[derive(Debug, Clone)]
42pub struct TypeErrorSpan {
43    pub line: usize,
44    pub col: usize,
45    pub label: String,
46}
47
48/// Result of type-checking that also carries memo-safety metadata.
49#[derive(Debug)]
50pub struct TypeCheckResult {
51    pub errors: Vec<TypeError>,
52    /// For each user-defined fn: (param_types, return_type, effects).
53    /// Used by the memo system to decide which fns qualify.
54    pub fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)>,
55    /// Set of type names whose values are memo-safe (hashable scalars / records of scalars).
56    pub memo_safe_types: HashSet<String>,
57    /// Unused binding warnings: (binding_name, fn_name, line).
58    pub unused_bindings: Vec<(String, String, usize)>,
59}
60
61pub fn run_type_check(items: &[TopLevel]) -> Vec<TypeError> {
62    run_type_check_with_base(items, None)
63}
64
65pub fn run_type_check_with_base(items: &[TopLevel], base_dir: Option<&str>) -> Vec<TypeError> {
66    run_type_check_full(items, base_dir).errors
67}
68
69pub fn run_type_check_full(items: &[TopLevel], base_dir: Option<&str>) -> TypeCheckResult {
70    let mut checker = TypeChecker::new();
71    checker.check(items, base_dir);
72    finalize_check_result(checker, items)
73}
74
75/// Variant of [`run_type_check_full`] that uses pre-loaded dependency
76/// modules instead of resolving them from disk. The playground feeds
77/// this from its in-memory virtual fs so multi-file projects type-
78/// check without any filesystem access.
79pub fn run_type_check_with_loaded(
80    items: &[TopLevel],
81    loaded: &[crate::source::LoadedModule],
82) -> TypeCheckResult {
83    let mut checker = TypeChecker::new();
84    checker.check_with_loaded(items, loaded);
85    finalize_check_result(checker, items)
86}
87
88fn finalize_check_result(checker: TypeChecker, items: &[TopLevel]) -> TypeCheckResult {
89    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
90        .fn_sigs
91        .iter()
92        .map(|(k, v)| {
93            (
94                k.clone(),
95                (v.params.clone(), v.ret.clone(), v.effects.clone()),
96            )
97        })
98        .collect();
99
100    let memo_safe_types = checker.compute_memo_safe_types(items);
101
102    TypeCheckResult {
103        errors: checker.errors,
104        fn_sigs,
105        memo_safe_types,
106        unused_bindings: checker.unused_warnings,
107    }
108}
109
110// ---------------------------------------------------------------------------
111// Internal structures
112// ---------------------------------------------------------------------------
113
114#[derive(Debug, Clone)]
115struct FnSig {
116    params: Vec<Type>,
117    ret: Type,
118    effects: Vec<String>,
119}
120
121struct TypeChecker {
122    fn_sigs: HashMap<String, FnSig>,
123    value_members: HashMap<String, Type>,
124    /// Field types for record types: "TypeName.fieldName" → Type.
125    /// Populated for both user-defined `record` types and built-in records
126    /// (HttpResponse, Header). Enables checked dot-access on Named types.
127    record_field_types: HashMap<String, Type>,
128    /// Unqualified → qualified aliases for cross-module lookups.
129    /// E.g. "Shape.Circle" → "Data.Shape.Circle".
130    sig_aliases: HashMap<String, String>,
131    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
132    /// Pre-populated for Result and Option; extended by user-defined sum types.
133    type_variants: HashMap<String, Vec<String>>,
134    /// Top-level bindings visible from function bodies.
135    globals: HashMap<String, Type>,
136    /// Local bindings in the current function/scope.
137    locals: HashMap<String, Type>,
138    errors: Vec<TypeError>,
139    /// Return type of the function currently being checked; None at top level.
140    current_fn_ret: Option<Type>,
141    /// Line number of the function currently being checked; None at top level.
142    current_fn_line: Option<usize>,
143    /// Type names that are opaque in this module's context (imported via `exposes opaque`).
144    opaque_types: HashSet<String>,
145    /// Names referenced during type checking of current function body (for unused detection).
146    used_names: HashSet<String>,
147    /// Bindings defined in the current function body: (name, line).
148    fn_bindings: Vec<(String, usize)>,
149    /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
150    unused_warnings: Vec<(String, String, usize)>,
151}
152
153impl TypeChecker {
154    fn new() -> Self {
155        let mut type_variants = HashMap::new();
156        type_variants.insert(
157            "Result".to_string(),
158            vec!["Ok".to_string(), "Err".to_string()],
159        );
160        type_variants.insert(
161            "Option".to_string(),
162            vec!["Some".to_string(), "None".to_string()],
163        );
164
165        let mut tc = TypeChecker {
166            fn_sigs: HashMap::new(),
167            value_members: HashMap::new(),
168            record_field_types: HashMap::new(),
169            sig_aliases: HashMap::new(),
170            type_variants,
171            globals: HashMap::new(),
172            locals: HashMap::new(),
173            errors: Vec::new(),
174            current_fn_ret: None,
175            current_fn_line: None,
176            opaque_types: HashSet::new(),
177            used_names: HashSet::new(),
178            fn_bindings: Vec::new(),
179            unused_warnings: Vec::new(),
180        };
181        tc.register_builtins();
182        tc
183    }
184
185    // -- Alias-aware lookups ------------------------------------------------
186
187    fn find_fn_sig(&self, key: &str) -> Option<&FnSig> {
188        self.fn_sigs
189            .get(key)
190            .or_else(|| self.sig_aliases.get(key).and_then(|c| self.fn_sigs.get(c)))
191    }
192
193    fn find_value_member(&self, key: &str) -> Option<&Type> {
194        self.value_members.get(key).or_else(|| {
195            self.sig_aliases
196                .get(key)
197                .and_then(|c| self.value_members.get(c))
198        })
199    }
200
201    fn find_record_field_type(&self, key: &str) -> Option<&Type> {
202        self.record_field_types.get(key).or_else(|| {
203            self.sig_aliases
204                .get(key)
205                .and_then(|c| self.record_field_types.get(c))
206        })
207    }
208
209    // -- Helpers -----------------------------------------------------------
210
211    /// Check whether `required_effect` is satisfied by `caller_effects`.
212    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
213        caller_effects
214            .iter()
215            .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
216    }
217
218    fn error(&mut self, msg: impl Into<String>) {
219        let line = self.current_fn_line.unwrap_or(1);
220        self.errors.push(TypeError {
221            message: msg.into(),
222            line,
223            col: 0,
224            secondary: None,
225        });
226    }
227
228    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
229        self.errors.push(TypeError {
230            message: msg.into(),
231            line,
232            col: 0,
233            secondary: None,
234        });
235    }
236
237    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
238        self.fn_sigs.insert(
239            name.to_string(),
240            FnSig {
241                params: params.to_vec(),
242                ret,
243                effects: effects.iter().map(|s| s.to_string()).collect(),
244            },
245        );
246    }
247
248    fn fn_type_from_sig(sig: &FnSig) -> Type {
249        Type::Fn(
250            sig.params.clone(),
251            Box::new(sig.ret.clone()),
252            sig.effects.clone(),
253        )
254    }
255
256    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
257        match ty {
258            Type::Fn(params, ret, effects) => Some(FnSig {
259                params: params.clone(),
260                ret: *ret.clone(),
261                effects: effects.clone(),
262            }),
263            _ => None,
264        }
265    }
266
267    fn binding_type(&self, name: &str) -> Option<Type> {
268        self.locals
269            .get(name)
270            .or_else(|| self.globals.get(name))
271            .cloned()
272    }
273
274    /// Compatibility used for checker constraints (call args, returns, ascriptions).
275    ///
276    /// We keep gradual typing for nested placeholders (`Result<Int, Unknown>` can
277    /// still fit `Result<Int, String>`), but reject *bare* `Unknown` when a
278    /// concrete type is required. This closes common false negatives where an
279    /// unresolved expression silently passes a concrete signature.
280    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
281        if matches!(actual, Type::Unknown) && !matches!(expected, Type::Unknown) {
282            return false;
283        }
284        actual.compatible(expected)
285    }
286}