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 resolves named generic variables at call sites. Error recovery
9/// uses `Type::Invalid`, which never satisfies a concrete expected type.
10use std::collections::{HashMap, HashSet};
11
12use super::{Type, parse_type_str_strict};
13use crate::ast::{
14    BinOp, Expr, FnDef, Literal, Module, Pattern, Spanned, Stmt, TailCallData, TopLevel, TypeDef,
15};
16
17mod builtins;
18pub mod effect_classification;
19pub mod effect_lifting;
20mod exhaustiveness;
21mod flow;
22pub mod hostile_effects;
23pub mod hostile_values;
24mod infer;
25mod memo;
26mod modules;
27pub mod oracle_subtypes;
28pub mod proof_trust_header;
29
30#[cfg(test)]
31mod tests;
32
33// ---------------------------------------------------------------------------
34// Public API
35// ---------------------------------------------------------------------------
36
37#[derive(Debug, Clone)]
38pub struct TypeError {
39    pub message: String,
40    pub line: usize,
41    pub col: usize,
42    /// Optional secondary span for multi-region diagnostics (e.g. declared type vs actual return).
43    pub secondary: Option<TypeErrorSpan>,
44}
45
46#[derive(Debug, Clone)]
47pub struct TypeErrorSpan {
48    pub line: usize,
49    pub col: usize,
50    pub label: String,
51}
52
53/// Result of type-checking that also carries memo-safety metadata.
54#[derive(Debug)]
55pub struct TypeCheckResult {
56    pub errors: Vec<TypeError>,
57    /// For each user-defined fn: (param_types, return_type, effects).
58    /// Used by the memo system to decide which fns qualify.
59    pub fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)>,
60    /// Set of type names whose values are memo-safe (hashable scalars / records of scalars).
61    pub memo_safe_types: HashSet<String>,
62    /// Unused binding warnings: (binding_name, fn_name, line).
63    pub unused_bindings: Vec<(String, String, usize)>,
64}
65
66pub fn run_type_check(items: &[TopLevel]) -> Vec<TypeError> {
67    run_type_check_with_base(items, None)
68}
69
70pub fn run_type_check_with_base(items: &[TopLevel], base_dir: Option<&str>) -> Vec<TypeError> {
71    run_type_check_full(items, base_dir).errors
72}
73
74pub fn run_type_check_full(items: &[TopLevel], base_dir: Option<&str>) -> TypeCheckResult {
75    let mut checker = TypeChecker::new();
76    checker.check(items, base_dir);
77    finalize_check_result(checker, items)
78}
79
80/// Variant of [`run_type_check_full`] that uses pre-loaded dependency
81/// modules instead of resolving them from disk. The playground feeds
82/// this from its in-memory virtual fs so multi-file projects type-
83/// check without any filesystem access.
84pub fn run_type_check_with_loaded(
85    items: &[TopLevel],
86    loaded: &[crate::source::LoadedModule],
87) -> TypeCheckResult {
88    let mut checker = TypeChecker::new();
89    checker.check_with_loaded(items, loaded);
90    finalize_check_result(checker, items)
91}
92
93fn finalize_check_result(mut checker: TypeChecker, items: &[TopLevel]) -> TypeCheckResult {
94    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
95        .fn_sigs
96        .iter()
97        .map(|(k, v)| {
98            (
99                k.clone(),
100                (v.params.clone(), v.ret.clone(), v.effects.clone()),
101            )
102        })
103        .collect();
104
105    let memo_safe_types = checker.compute_memo_safe_types(items);
106
107    check_module_effect_boundary(items, &mut checker.errors);
108
109    TypeCheckResult {
110        errors: checker.errors,
111        fn_sigs,
112        memo_safe_types,
113        unused_bindings: checker.unused_warnings,
114    }
115}
116
117/// Enforce module-level `effects [...]` declaration against per-fn effect
118/// usage. The rule:
119///
120/// - Module without `effects [...]` → legacy/mixed, no enforcement (0.13
121///   migration shim; 0.14+ may upgrade to soft warning).
122/// - Module with `effects [...]` (including `effects []` for explicit pure)
123///   → every function's `! [...]` must be covered by the module's declared
124///   surface. A namespace-level entry like `Disk` admits any `Disk.*`
125///   method; a method-level entry like `Time.now` admits only that one.
126fn check_module_effect_boundary(items: &[TopLevel], errors: &mut Vec<TypeError>) {
127    let Some(allowed) = items.iter().find_map(|i| match i {
128        TopLevel::Module(m) => m.effects.as_ref().map(|e| (e, m)),
129        _ => None,
130    }) else {
131        return;
132    };
133    let (allowed_list, module) = allowed;
134
135    let allowed_namespaces: HashSet<&str> = allowed_list
136        .iter()
137        .filter(|e| !e.contains('.'))
138        .map(|e| e.as_str())
139        .collect();
140    let allowed_methods: HashSet<&str> = allowed_list.iter().map(|e| e.as_str()).collect();
141
142    for item in items {
143        let TopLevel::FnDef(fd) = item else { continue };
144        for eff in &fd.effects {
145            let method = eff.node.as_str();
146            if allowed_methods.contains(method) {
147                continue;
148            }
149            if let Some((ns, _)) = method.split_once('.')
150                && allowed_namespaces.contains(ns)
151            {
152                continue;
153            }
154            errors.push(TypeError {
155                message: format!(
156                    "module '{}' declared `effects [{}]` but '{}' uses '{}' which is not in the declared boundary",
157                    module.name,
158                    allowed_list.join(", "),
159                    fd.name,
160                    method
161                ),
162                line: eff.line,
163                col: 1,
164                secondary: module.effects_line.map(|l| TypeErrorSpan {
165                    line: l,
166                    col: 1,
167                    label: "module effects declared here".to_string(),
168                }),
169            });
170        }
171    }
172}
173
174// ---------------------------------------------------------------------------
175// Internal structures
176// ---------------------------------------------------------------------------
177
178#[derive(Debug, Clone)]
179struct FnSig {
180    params: Vec<Type>,
181    ret: Type,
182    effects: Vec<String>,
183}
184
185struct TypeChecker {
186    fn_sigs: HashMap<String, FnSig>,
187    value_members: HashMap<String, Type>,
188    /// Field types for record types: "TypeName.fieldName" → Type.
189    /// Populated for both user-defined `record` types and built-in records
190    /// (HttpResponse, Header). Enables checked dot-access on Named types.
191    record_field_types: HashMap<String, Type>,
192    /// Unqualified → qualified aliases for cross-module lookups.
193    /// E.g. "Shape.Circle" → "Data.Shape.Circle".
194    sig_aliases: HashMap<String, String>,
195    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
196    /// Pre-populated for Result and Option; extended by user-defined sum types.
197    type_variants: HashMap<String, Vec<String>>,
198    /// Top-level bindings visible from function bodies.
199    globals: HashMap<String, Type>,
200    /// Local bindings in the current function/scope.
201    locals: HashMap<String, Type>,
202    errors: Vec<TypeError>,
203    /// Return type of the function currently being checked; None at top level.
204    current_fn_ret: Option<Type>,
205    /// Line number of the function currently being checked; None at top level.
206    current_fn_line: Option<usize>,
207    /// Type names that are opaque in this module's context (imported via `exposes opaque`).
208    opaque_types: HashSet<String>,
209    /// Names referenced during type checking of current function body (for unused detection).
210    used_names: HashSet<String>,
211    /// Bindings defined in the current function body: (name, line).
212    fn_bindings: Vec<(String, usize)>,
213    /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
214    unused_warnings: Vec<(String, String, usize)>,
215    /// Oracle v1: `.result` / `.trace` / `.trace.*` projections are
216    /// only meaningful inside `verify <fn> trace` cases. This flag is
217    /// set true while checking such a case's LHS / RHS, false
218    /// otherwise. Outside verify-trace the projections are rejected at
219    /// check time — otherwise user code would type-check then crash
220    /// at runtime with "namespace has no member 'trace'".
221    in_verify_trace_context: bool,
222}
223
224impl TypeChecker {
225    fn new() -> Self {
226        let mut type_variants = HashMap::new();
227        type_variants.insert(
228            "Result".to_string(),
229            vec!["Ok".to_string(), "Err".to_string()],
230        );
231        type_variants.insert(
232            "Option".to_string(),
233            vec!["Some".to_string(), "None".to_string()],
234        );
235
236        let mut tc = TypeChecker {
237            fn_sigs: HashMap::new(),
238            value_members: HashMap::new(),
239            record_field_types: HashMap::new(),
240            sig_aliases: HashMap::new(),
241            type_variants,
242            globals: HashMap::new(),
243            locals: HashMap::new(),
244            errors: Vec::new(),
245            current_fn_ret: None,
246            current_fn_line: None,
247            opaque_types: HashSet::new(),
248            used_names: HashSet::new(),
249            fn_bindings: Vec::new(),
250            unused_warnings: Vec::new(),
251            in_verify_trace_context: false,
252        };
253        tc.register_builtins();
254        tc
255    }
256
257    // -- Alias-aware lookups ------------------------------------------------
258
259    fn find_fn_sig(&self, key: &str) -> Option<&FnSig> {
260        self.fn_sigs
261            .get(key)
262            .or_else(|| self.sig_aliases.get(key).and_then(|c| self.fn_sigs.get(c)))
263    }
264
265    fn find_value_member(&self, key: &str) -> Option<&Type> {
266        self.value_members.get(key).or_else(|| {
267            self.sig_aliases
268                .get(key)
269                .and_then(|c| self.value_members.get(c))
270        })
271    }
272
273    fn find_record_field_type(&self, key: &str) -> Option<&Type> {
274        self.record_field_types.get(key).or_else(|| {
275            self.sig_aliases
276                .get(key)
277                .and_then(|c| self.record_field_types.get(c))
278        })
279    }
280
281    // -- Helpers -----------------------------------------------------------
282
283    /// Check whether `required_effect` is satisfied by `caller_effects`.
284    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
285        caller_effects
286            .iter()
287            .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
288    }
289
290    fn error(&mut self, msg: impl Into<String>) {
291        let line = self.current_fn_line.unwrap_or(1);
292        self.errors.push(TypeError {
293            message: msg.into(),
294            line,
295            col: 0,
296            secondary: None,
297        });
298    }
299
300    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
301        self.errors.push(TypeError {
302            message: msg.into(),
303            line,
304            col: 0,
305            secondary: None,
306        });
307    }
308
309    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
310        self.fn_sigs.insert(
311            name.to_string(),
312            FnSig {
313                params: params.to_vec(),
314                ret,
315                effects: effects.iter().map(|s| s.to_string()).collect(),
316            },
317        );
318    }
319
320    fn fn_type_from_sig(sig: &FnSig) -> Type {
321        Type::Fn(
322            sig.params.clone(),
323            Box::new(sig.ret.clone()),
324            sig.effects.clone(),
325        )
326    }
327
328    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
329        match ty {
330            Type::Fn(params, ret, effects) => Some(FnSig {
331                params: params.clone(),
332                ret: *ret.clone(),
333                effects: effects.clone(),
334            }),
335            _ => None,
336        }
337    }
338
339    fn binding_type(&self, name: &str) -> Option<Type> {
340        self.locals
341            .get(name)
342            .or_else(|| self.globals.get(name))
343            .cloned()
344    }
345
346    /// Compatibility used for checker constraints (returns, ascriptions, and
347    /// simple non-polymorphic call args). Variables in the expected type may bind
348    /// to the actual type; variables in the actual type only satisfy the exact
349    /// same expected variable and never satisfy concrete requirements.
350    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
351        let mut subst = HashMap::new();
352        Self::match_expected_type(actual, expected, &mut subst)
353    }
354
355    pub(super) fn match_expected_type(
356        actual: &Type,
357        expected: &Type,
358        subst: &mut HashMap<String, Type>,
359    ) -> bool {
360        match expected {
361            Type::Var(name) => Self::bind_expected_var(name, actual, subst),
362            Type::Invalid => false,
363            Type::Int => matches!(actual, Type::Int),
364            Type::Float => matches!(actual, Type::Float),
365            Type::Str => matches!(actual, Type::Str),
366            Type::Bool => matches!(actual, Type::Bool),
367            Type::Unit => matches!(actual, Type::Unit),
368            Type::Named(expected_name) => match actual {
369                Type::Named(actual_name) => {
370                    actual_name == expected_name
371                        || actual_name.ends_with(&format!(".{}", expected_name))
372                        || expected_name.ends_with(&format!(".{}", actual_name))
373                }
374                _ => false,
375            },
376            Type::Option(expected_inner) => match actual {
377                Type::Option(actual_inner) => {
378                    Self::match_expected_type(actual_inner, expected_inner, subst)
379                }
380                _ => false,
381            },
382            Type::List(expected_inner) => match actual {
383                Type::List(actual_inner) => {
384                    Self::match_expected_type(actual_inner, expected_inner, subst)
385                }
386                _ => false,
387            },
388            Type::Vector(expected_inner) => match actual {
389                Type::Vector(actual_inner) => {
390                    Self::match_expected_type(actual_inner, expected_inner, subst)
391                }
392                _ => false,
393            },
394            Type::Result(expected_ok, expected_err) => match actual {
395                Type::Result(actual_ok, actual_err) => {
396                    Self::match_expected_type(actual_ok, expected_ok, subst)
397                        && Self::match_expected_type(actual_err, expected_err, subst)
398                }
399                _ => false,
400            },
401            Type::Map(expected_k, expected_v) => match actual {
402                Type::Map(actual_k, actual_v) => {
403                    Self::match_expected_type(actual_k, expected_k, subst)
404                        && Self::match_expected_type(actual_v, expected_v, subst)
405                }
406                _ => false,
407            },
408            Type::Tuple(expected_items) => match actual {
409                Type::Tuple(actual_items) if actual_items.len() == expected_items.len() => {
410                    actual_items.iter().zip(expected_items.iter()).all(
411                        |(actual_item, expected_item)| {
412                            Self::match_expected_type(actual_item, expected_item, subst)
413                        },
414                    )
415                }
416                _ => false,
417            },
418            Type::Fn(expected_params, expected_ret, expected_effects) => match actual {
419                Type::Fn(actual_params, actual_ret, actual_effects)
420                    if actual_params.len() == expected_params.len() =>
421                {
422                    actual_params.iter().zip(expected_params.iter()).all(
423                        |(actual_param, expected_param)| {
424                            Self::match_expected_type(actual_param, expected_param, subst)
425                        },
426                    ) && Self::match_expected_type(actual_ret, expected_ret, subst)
427                        && actual_effects.iter().all(|actual| {
428                            expected_effects
429                                .iter()
430                                .any(|expected| crate::effects::effect_satisfies(expected, actual))
431                        })
432                }
433                _ => false,
434            },
435        }
436    }
437
438    fn bind_expected_var(name: &str, actual: &Type, subst: &mut HashMap<String, Type>) -> bool {
439        match actual {
440            Type::Var(actual_name) => return actual_name == name,
441            Type::Invalid => return false,
442            _ => {}
443        }
444        if let Some(bound) = subst.get(name).cloned() {
445            return Self::match_expected_type(actual, &bound, subst)
446                && Self::match_expected_type(&bound, actual, subst);
447        }
448        subst.insert(name.to_string(), actual.clone());
449        true
450    }
451
452    pub(super) fn instantiate_type(ty: &Type, subst: &HashMap<String, Type>) -> Type {
453        match ty {
454            Type::Var(name) => subst.get(name).cloned().unwrap_or_else(|| ty.clone()),
455            Type::Result(ok, err) => Type::Result(
456                Box::new(Self::instantiate_type(ok, subst)),
457                Box::new(Self::instantiate_type(err, subst)),
458            ),
459            Type::Option(inner) => Type::Option(Box::new(Self::instantiate_type(inner, subst))),
460            Type::List(inner) => Type::List(Box::new(Self::instantiate_type(inner, subst))),
461            Type::Vector(inner) => Type::Vector(Box::new(Self::instantiate_type(inner, subst))),
462            Type::Map(k, v) => Type::Map(
463                Box::new(Self::instantiate_type(k, subst)),
464                Box::new(Self::instantiate_type(v, subst)),
465            ),
466            Type::Tuple(items) => Type::Tuple(
467                items
468                    .iter()
469                    .map(|item| Self::instantiate_type(item, subst))
470                    .collect(),
471            ),
472            Type::Fn(params, ret, effects) => Type::Fn(
473                params
474                    .iter()
475                    .map(|param| Self::instantiate_type(param, subst))
476                    .collect(),
477                Box::new(Self::instantiate_type(ret, subst)),
478                effects.clone(),
479            ),
480            Type::Int
481            | Type::Float
482            | Type::Str
483            | Type::Bool
484            | Type::Unit
485            | Type::Invalid
486            | Type::Named(_) => ty.clone(),
487        }
488    }
489}