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};
12use std::path::Path;
13
14use super::{Type, parse_type_str_strict};
15use crate::ast::{BinOp, Expr, FnBody, FnDef, Literal, Module, Pattern, Stmt, TopLevel, TypeDef};
16use crate::source::{
17    canonicalize_path, find_module_file, parse_source, require_module_declaration,
18};
19
20mod builtins;
21mod exhaustiveness;
22mod flow;
23mod infer;
24mod memo;
25mod modules;
26
27#[cfg(test)]
28mod tests;
29
30// ---------------------------------------------------------------------------
31// Public API
32// ---------------------------------------------------------------------------
33
34#[derive(Debug, Clone)]
35pub struct TypeError {
36    pub message: String,
37    pub line: usize,
38    pub col: usize,
39}
40
41/// Result of type-checking that also carries memo-safety metadata.
42#[derive(Debug)]
43pub struct TypeCheckResult {
44    pub errors: Vec<TypeError>,
45    /// For each user-defined fn: (param_types, return_type, effects).
46    /// Used by the memo system to decide which fns qualify.
47    pub fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)>,
48    /// Set of type names whose values are memo-safe (hashable scalars / records of scalars).
49    pub memo_safe_types: HashSet<String>,
50}
51
52pub fn run_type_check(items: &[TopLevel]) -> Vec<TypeError> {
53    run_type_check_with_base(items, None)
54}
55
56pub fn run_type_check_with_base(items: &[TopLevel], base_dir: Option<&str>) -> Vec<TypeError> {
57    run_type_check_full(items, base_dir).errors
58}
59
60pub fn run_type_check_full(items: &[TopLevel], base_dir: Option<&str>) -> TypeCheckResult {
61    let mut checker = TypeChecker::new();
62    checker.check(items, base_dir);
63
64    // Export fn_sigs for memo analysis
65    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
66        .fn_sigs
67        .iter()
68        .map(|(k, v)| {
69            (
70                k.clone(),
71                (v.params.clone(), v.ret.clone(), v.effects.clone()),
72            )
73        })
74        .collect();
75
76    // Compute memo-safe named types
77    let memo_safe_types = checker.compute_memo_safe_types(items);
78
79    TypeCheckResult {
80        errors: checker.errors,
81        fn_sigs,
82        memo_safe_types,
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Internal structures
88// ---------------------------------------------------------------------------
89
90#[derive(Debug, Clone)]
91struct FnSig {
92    params: Vec<Type>,
93    ret: Type,
94    effects: Vec<String>,
95}
96
97#[derive(Debug, Clone)]
98struct ModuleSigCache {
99    fn_entries: Vec<(String, FnSig)>,
100    value_entries: Vec<(String, Type)>,
101    record_field_entries: Vec<(String, Type)>,
102    type_variants: Vec<(String, Vec<String>)>,
103}
104
105struct TypeChecker {
106    fn_sigs: HashMap<String, FnSig>,
107    module_sig_cache: HashMap<String, ModuleSigCache>,
108    value_members: HashMap<String, Type>,
109    /// Field types for record types: "TypeName.fieldName" → Type.
110    /// Populated for both user-defined `record` types and built-in records
111    /// (HttpResponse, Header). Enables checked dot-access on Named types.
112    record_field_types: HashMap<String, Type>,
113    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
114    /// Pre-populated for Result and Option; extended by user-defined sum types.
115    type_variants: HashMap<String, Vec<String>>,
116    /// Named effect aliases: `effects AppIO = [Console, Disk]`
117    effect_aliases: HashMap<String, Vec<String>>,
118    /// Top-level bindings visible from function bodies.
119    globals: HashMap<String, Type>,
120    /// Local bindings in the current function/scope.
121    locals: HashMap<String, Type>,
122    errors: Vec<TypeError>,
123    /// Return type of the function currently being checked; None at top level.
124    current_fn_ret: Option<Type>,
125    /// Line number of the function currently being checked; None at top level.
126    current_fn_line: Option<usize>,
127}
128
129impl TypeChecker {
130    fn new() -> Self {
131        let mut type_variants = HashMap::new();
132        type_variants.insert(
133            "Result".to_string(),
134            vec!["Ok".to_string(), "Err".to_string()],
135        );
136        type_variants.insert(
137            "Option".to_string(),
138            vec!["Some".to_string(), "None".to_string()],
139        );
140
141        let mut tc = TypeChecker {
142            fn_sigs: HashMap::new(),
143            module_sig_cache: HashMap::new(),
144            value_members: HashMap::new(),
145            record_field_types: HashMap::new(),
146            type_variants,
147            effect_aliases: HashMap::new(),
148            globals: HashMap::new(),
149            locals: HashMap::new(),
150            errors: Vec::new(),
151            current_fn_ret: None,
152            current_fn_line: None,
153        };
154        tc.register_builtins();
155        tc
156    }
157
158    /// Expand a list of effect names, resolving any aliases one level deep.
159    fn expand_effects(&self, effects: &[String]) -> Vec<String> {
160        let mut result = Vec::new();
161        for e in effects {
162            if let Some(expanded) = self.effect_aliases.get(e) {
163                result.extend(expanded.iter().cloned());
164            } else {
165                result.push(e.clone());
166            }
167        }
168        result
169    }
170
171    /// Check whether `required_effect` is satisfied by `caller_effects` (with alias expansion).
172    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
173        let expanded_caller = self.expand_effects(caller_effects);
174        // Also expand the required effect (in case it's itself an alias)
175        let expanded_required = self.expand_effects(&[required_effect.to_string()]);
176        expanded_required
177            .iter()
178            .all(|e| expanded_caller.contains(e))
179    }
180
181    fn error(&mut self, msg: impl Into<String>) {
182        let line = self.current_fn_line.unwrap_or(1);
183        self.errors.push(TypeError {
184            message: msg.into(),
185            line,
186            col: 0,
187        });
188    }
189
190    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
191        self.errors.push(TypeError {
192            message: msg.into(),
193            line,
194            col: 0,
195        });
196    }
197
198    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
199        self.fn_sigs.insert(
200            name.to_string(),
201            FnSig {
202                params: params.to_vec(),
203                ret,
204                effects: effects.iter().map(|s| s.to_string()).collect(),
205            },
206        );
207    }
208
209    fn fn_type_from_sig(sig: &FnSig) -> Type {
210        Type::Fn(
211            sig.params.clone(),
212            Box::new(sig.ret.clone()),
213            sig.effects.clone(),
214        )
215    }
216
217    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
218        match ty {
219            Type::Fn(params, ret, effects) => Some(FnSig {
220                params: params.clone(),
221                ret: *ret.clone(),
222                effects: effects.clone(),
223            }),
224            _ => None,
225        }
226    }
227
228    fn binding_type(&self, name: &str) -> Option<Type> {
229        self.locals
230            .get(name)
231            .or_else(|| self.globals.get(name))
232            .cloned()
233    }
234
235    /// Compatibility used for checker constraints (call args, returns, ascriptions).
236    ///
237    /// We keep gradual typing for nested placeholders (`Result<Int, Unknown>` can
238    /// still fit `Result<Int, String>`), but reject *bare* `Unknown` when a
239    /// concrete type is required. This closes common false negatives where an
240    /// unresolved expression silently passes a concrete signature.
241    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
242        if matches!(actual, Type::Unknown) && !matches!(expected, Type::Unknown) {
243            return false;
244        }
245        actual.compatible(expected)
246    }
247}