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