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
73    // Export fn_sigs for memo analysis
74    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
75        .fn_sigs
76        .iter()
77        .map(|(k, v)| {
78            (
79                k.clone(),
80                (v.params.clone(), v.ret.clone(), v.effects.clone()),
81            )
82        })
83        .collect();
84
85    // Compute memo-safe named types
86    let memo_safe_types = checker.compute_memo_safe_types(items);
87
88    TypeCheckResult {
89        errors: checker.errors,
90        fn_sigs,
91        memo_safe_types,
92        unused_bindings: checker.unused_warnings,
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Internal structures
98// ---------------------------------------------------------------------------
99
100#[derive(Debug, Clone)]
101struct FnSig {
102    params: Vec<Type>,
103    ret: Type,
104    effects: Vec<String>,
105}
106
107struct TypeChecker {
108    fn_sigs: HashMap<String, FnSig>,
109    value_members: HashMap<String, Type>,
110    /// Field types for record types: "TypeName.fieldName" → Type.
111    /// Populated for both user-defined `record` types and built-in records
112    /// (HttpResponse, Header). Enables checked dot-access on Named types.
113    record_field_types: HashMap<String, Type>,
114    /// Unqualified → qualified aliases for cross-module lookups.
115    /// E.g. "Shape.Circle" → "Data.Shape.Circle".
116    sig_aliases: HashMap<String, String>,
117    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
118    /// Pre-populated for Result and Option; extended by user-defined sum types.
119    type_variants: HashMap<String, Vec<String>>,
120    /// Top-level bindings visible from function bodies.
121    globals: HashMap<String, Type>,
122    /// Local bindings in the current function/scope.
123    locals: HashMap<String, Type>,
124    errors: Vec<TypeError>,
125    /// Return type of the function currently being checked; None at top level.
126    current_fn_ret: Option<Type>,
127    /// Line number of the function currently being checked; None at top level.
128    current_fn_line: Option<usize>,
129    /// Type names that are opaque in this module's context (imported via `exposes opaque`).
130    opaque_types: HashSet<String>,
131    /// Names referenced during type checking of current function body (for unused detection).
132    used_names: HashSet<String>,
133    /// Bindings defined in the current function body: (name, line).
134    fn_bindings: Vec<(String, usize)>,
135    /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
136    unused_warnings: Vec<(String, String, usize)>,
137}
138
139impl TypeChecker {
140    fn new() -> Self {
141        let mut type_variants = HashMap::new();
142        type_variants.insert(
143            "Result".to_string(),
144            vec!["Ok".to_string(), "Err".to_string()],
145        );
146        type_variants.insert(
147            "Option".to_string(),
148            vec!["Some".to_string(), "None".to_string()],
149        );
150
151        let mut tc = TypeChecker {
152            fn_sigs: HashMap::new(),
153            value_members: HashMap::new(),
154            record_field_types: HashMap::new(),
155            sig_aliases: HashMap::new(),
156            type_variants,
157            globals: HashMap::new(),
158            locals: HashMap::new(),
159            errors: Vec::new(),
160            current_fn_ret: None,
161            current_fn_line: None,
162            opaque_types: HashSet::new(),
163            used_names: HashSet::new(),
164            fn_bindings: Vec::new(),
165            unused_warnings: Vec::new(),
166        };
167        tc.register_builtins();
168        tc
169    }
170
171    // -- Alias-aware lookups ------------------------------------------------
172
173    fn find_fn_sig(&self, key: &str) -> Option<&FnSig> {
174        self.fn_sigs
175            .get(key)
176            .or_else(|| self.sig_aliases.get(key).and_then(|c| self.fn_sigs.get(c)))
177    }
178
179    fn find_value_member(&self, key: &str) -> Option<&Type> {
180        self.value_members.get(key).or_else(|| {
181            self.sig_aliases
182                .get(key)
183                .and_then(|c| self.value_members.get(c))
184        })
185    }
186
187    fn find_record_field_type(&self, key: &str) -> Option<&Type> {
188        self.record_field_types.get(key).or_else(|| {
189            self.sig_aliases
190                .get(key)
191                .and_then(|c| self.record_field_types.get(c))
192        })
193    }
194
195    // -- Helpers -----------------------------------------------------------
196
197    /// Check whether `required_effect` is satisfied by `caller_effects`.
198    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
199        caller_effects
200            .iter()
201            .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
202    }
203
204    fn error(&mut self, msg: impl Into<String>) {
205        let line = self.current_fn_line.unwrap_or(1);
206        self.errors.push(TypeError {
207            message: msg.into(),
208            line,
209            col: 0,
210            secondary: None,
211        });
212    }
213
214    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
215        self.errors.push(TypeError {
216            message: msg.into(),
217            line,
218            col: 0,
219            secondary: None,
220        });
221    }
222
223    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
224        self.fn_sigs.insert(
225            name.to_string(),
226            FnSig {
227                params: params.to_vec(),
228                ret,
229                effects: effects.iter().map(|s| s.to_string()).collect(),
230            },
231        );
232    }
233
234    fn fn_type_from_sig(sig: &FnSig) -> Type {
235        Type::Fn(
236            sig.params.clone(),
237            Box::new(sig.ret.clone()),
238            sig.effects.clone(),
239        )
240    }
241
242    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
243        match ty {
244            Type::Fn(params, ret, effects) => Some(FnSig {
245                params: params.clone(),
246                ret: *ret.clone(),
247                effects: effects.clone(),
248            }),
249            _ => None,
250        }
251    }
252
253    fn binding_type(&self, name: &str) -> Option<Type> {
254        self.locals
255            .get(name)
256            .or_else(|| self.globals.get(name))
257            .cloned()
258    }
259
260    /// Compatibility used for checker constraints (call args, returns, ascriptions).
261    ///
262    /// We keep gradual typing for nested placeholders (`Result<Int, Unknown>` can
263    /// still fit `Result<Int, String>`), but reject *bare* `Unknown` when a
264    /// concrete type is required. This closes common false negatives where an
265    /// unresolved expression silently passes a concrete signature.
266    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
267        if matches!(actual, Type::Unknown) && !matches!(expected, Type::Unknown) {
268            return false;
269        }
270        actual.compatible(expected)
271    }
272}