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