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, FnDef, Literal, Module, Pattern, Spanned, 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    /// Optional secondary span for multi-region diagnostics (e.g. declared type vs actual return).
40    pub secondary: Option<TypeErrorSpan>,
41}
42
43#[derive(Debug, Clone)]
44pub struct TypeErrorSpan {
45    pub line: usize,
46    pub col: usize,
47    pub label: String,
48}
49
50/// Result of type-checking that also carries memo-safety metadata.
51#[derive(Debug)]
52pub struct TypeCheckResult {
53    pub errors: Vec<TypeError>,
54    /// For each user-defined fn: (param_types, return_type, effects).
55    /// Used by the memo system to decide which fns qualify.
56    pub fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)>,
57    /// Set of type names whose values are memo-safe (hashable scalars / records of scalars).
58    pub memo_safe_types: HashSet<String>,
59    /// Unused binding warnings: (binding_name, fn_name, line).
60    pub unused_bindings: Vec<(String, String, usize)>,
61}
62
63pub fn run_type_check(items: &[TopLevel]) -> Vec<TypeError> {
64    run_type_check_with_base(items, None)
65}
66
67pub fn run_type_check_with_base(items: &[TopLevel], base_dir: Option<&str>) -> Vec<TypeError> {
68    run_type_check_full(items, base_dir).errors
69}
70
71pub fn run_type_check_full(items: &[TopLevel], base_dir: Option<&str>) -> TypeCheckResult {
72    let mut checker = TypeChecker::new();
73    checker.check(items, base_dir);
74
75    // Export fn_sigs for memo analysis
76    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
77        .fn_sigs
78        .iter()
79        .map(|(k, v)| {
80            (
81                k.clone(),
82                (v.params.clone(), v.ret.clone(), v.effects.clone()),
83            )
84        })
85        .collect();
86
87    // Compute memo-safe named types
88    let memo_safe_types = checker.compute_memo_safe_types(items);
89
90    TypeCheckResult {
91        errors: checker.errors,
92        fn_sigs,
93        memo_safe_types,
94        unused_bindings: checker.unused_warnings,
95    }
96}
97
98// ---------------------------------------------------------------------------
99// Internal structures
100// ---------------------------------------------------------------------------
101
102#[derive(Debug, Clone)]
103struct FnSig {
104    params: Vec<Type>,
105    ret: Type,
106    effects: Vec<String>,
107}
108
109#[derive(Debug, Clone)]
110struct ModuleSigCache {
111    fn_entries: Vec<(String, FnSig)>,
112    value_entries: Vec<(String, Type)>,
113    record_field_entries: Vec<(String, Type)>,
114    type_variants: Vec<(String, Vec<String>)>,
115    opaque_types: Vec<String>,
116}
117
118struct TypeChecker {
119    fn_sigs: HashMap<String, FnSig>,
120    module_sig_cache: HashMap<String, ModuleSigCache>,
121    value_members: HashMap<String, Type>,
122    /// Field types for record types: "TypeName.fieldName" → Type.
123    /// Populated for both user-defined `record` types and built-in records
124    /// (HttpResponse, Header). Enables checked dot-access on Named types.
125    record_field_types: HashMap<String, Type>,
126    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
127    /// Pre-populated for Result and Option; extended by user-defined sum types.
128    type_variants: HashMap<String, Vec<String>>,
129    /// Top-level bindings visible from function bodies.
130    globals: HashMap<String, Type>,
131    /// Local bindings in the current function/scope.
132    locals: HashMap<String, Type>,
133    errors: Vec<TypeError>,
134    /// Return type of the function currently being checked; None at top level.
135    current_fn_ret: Option<Type>,
136    /// Line number of the function currently being checked; None at top level.
137    current_fn_line: Option<usize>,
138    /// Type names that are opaque in this module's context (imported via `exposes opaque`).
139    opaque_types: HashSet<String>,
140    /// Names referenced during type checking of current function body (for unused detection).
141    used_names: HashSet<String>,
142    /// Bindings defined in the current function body: (name, line).
143    fn_bindings: Vec<(String, usize)>,
144    /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
145    unused_warnings: Vec<(String, String, usize)>,
146}
147
148impl TypeChecker {
149    fn new() -> Self {
150        let mut type_variants = HashMap::new();
151        type_variants.insert(
152            "Result".to_string(),
153            vec!["Ok".to_string(), "Err".to_string()],
154        );
155        type_variants.insert(
156            "Option".to_string(),
157            vec!["Some".to_string(), "None".to_string()],
158        );
159
160        let mut tc = TypeChecker {
161            fn_sigs: HashMap::new(),
162            module_sig_cache: HashMap::new(),
163            value_members: HashMap::new(),
164            record_field_types: HashMap::new(),
165            type_variants,
166            globals: HashMap::new(),
167            locals: HashMap::new(),
168            errors: Vec::new(),
169            current_fn_ret: None,
170            current_fn_line: None,
171            opaque_types: HashSet::new(),
172            used_names: HashSet::new(),
173            fn_bindings: Vec::new(),
174            unused_warnings: Vec::new(),
175        };
176        tc.register_builtins();
177        tc
178    }
179
180    /// Check whether `required_effect` is satisfied by `caller_effects`.
181    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
182        caller_effects
183            .iter()
184            .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
185    }
186
187    fn error(&mut self, msg: impl Into<String>) {
188        let line = self.current_fn_line.unwrap_or(1);
189        self.errors.push(TypeError {
190            message: msg.into(),
191            line,
192            col: 0,
193            secondary: None,
194        });
195    }
196
197    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
198        self.errors.push(TypeError {
199            message: msg.into(),
200            line,
201            col: 0,
202            secondary: None,
203        });
204    }
205
206    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
207        self.fn_sigs.insert(
208            name.to_string(),
209            FnSig {
210                params: params.to_vec(),
211                ret,
212                effects: effects.iter().map(|s| s.to_string()).collect(),
213            },
214        );
215    }
216
217    fn fn_type_from_sig(sig: &FnSig) -> Type {
218        Type::Fn(
219            sig.params.clone(),
220            Box::new(sig.ret.clone()),
221            sig.effects.clone(),
222        )
223    }
224
225    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
226        match ty {
227            Type::Fn(params, ret, effects) => Some(FnSig {
228                params: params.clone(),
229                ret: *ret.clone(),
230                effects: effects.clone(),
231            }),
232            _ => None,
233        }
234    }
235
236    fn binding_type(&self, name: &str) -> Option<Type> {
237        self.locals
238            .get(name)
239            .or_else(|| self.globals.get(name))
240            .cloned()
241    }
242
243    /// Compatibility used for checker constraints (call args, returns, ascriptions).
244    ///
245    /// We keep gradual typing for nested placeholders (`Result<Int, Unknown>` can
246    /// still fit `Result<Int, String>`), but reject *bare* `Unknown` when a
247    /// concrete type is required. This closes common false negatives where an
248    /// unresolved expression silently passes a concrete signature.
249    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
250        if matches!(actual, Type::Unknown) && !matches!(expected, Type::Unknown) {
251            return false;
252        }
253        actual.compatible(expected)
254    }
255}