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
93/// Self-host variant of [`run_type_check_full`]: bypasses the
94/// opaque-type checks (construction, field access, pattern match).
95/// Used exclusively by `aver compile --with-self-host-support` so
96/// `self_hosted/domain/builtins.av` can round-trip opaque host
97/// types (e.g. `Tcp.Connection`) through the replay JSON contract.
98/// User code outside the self-host always goes through the regular
99/// [`run_type_check_full`] and stays bound by the opaque rules.
100pub fn run_type_check_full_self_host(
101    items: &[TopLevel],
102    base_dir: Option<&str>,
103) -> TypeCheckResult {
104    let mut checker = TypeChecker::new();
105    checker.self_host_mode = true;
106    checker.check(items, base_dir);
107    finalize_check_result(checker, items)
108}
109
110/// Self-host variant of [`run_type_check_with_loaded`]. See
111/// [`run_type_check_full_self_host`] for the opaque-bypass rationale.
112pub fn run_type_check_with_loaded_self_host(
113    items: &[TopLevel],
114    loaded: &[crate::source::LoadedModule],
115) -> TypeCheckResult {
116    let mut checker = TypeChecker::new();
117    checker.self_host_mode = true;
118    checker.check_with_loaded(items, loaded);
119    finalize_check_result(checker, items)
120}
121
122fn finalize_check_result(mut checker: TypeChecker, items: &[TopLevel]) -> TypeCheckResult {
123    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
124        .fn_sigs
125        .iter()
126        .map(|(k, v)| {
127            (
128                k.clone(),
129                (v.params.clone(), v.ret.clone(), v.effects.clone()),
130            )
131        })
132        .collect();
133
134    let memo_safe_types = checker.compute_memo_safe_types(items);
135
136    check_module_effect_boundary(items, &mut checker.errors);
137
138    TypeCheckResult {
139        errors: checker.errors,
140        fn_sigs,
141        memo_safe_types,
142        unused_bindings: checker.unused_warnings,
143    }
144}
145
146/// Enforce module-level `effects [...]` declaration against per-fn effect
147/// usage. The rule:
148///
149/// - Module without `effects [...]` → legacy/mixed, no enforcement (0.13
150///   migration shim; 0.14+ may upgrade to soft warning).
151/// - Module with `effects [...]` (including `effects []` for explicit pure)
152///   → every function's `! [...]` must be covered by the module's declared
153///   surface. A namespace-level entry like `Disk` admits any `Disk.*`
154///   method; a method-level entry like `Time.now` admits only that one.
155fn check_module_effect_boundary(items: &[TopLevel], errors: &mut Vec<TypeError>) {
156    let Some(allowed) = items.iter().find_map(|i| match i {
157        TopLevel::Module(m) => m.effects.as_ref().map(|e| (e, m)),
158        _ => None,
159    }) else {
160        return;
161    };
162    let (allowed_list, module) = allowed;
163
164    let allowed_namespaces: HashSet<&str> = allowed_list
165        .iter()
166        .filter(|e| !e.contains('.'))
167        .map(|e| e.as_str())
168        .collect();
169    let allowed_methods: HashSet<&str> = allowed_list.iter().map(|e| e.as_str()).collect();
170
171    for item in items {
172        let TopLevel::FnDef(fd) = item else { continue };
173        for eff in &fd.effects {
174            let method = eff.node.as_str();
175            if allowed_methods.contains(method) {
176                continue;
177            }
178            if let Some((ns, _)) = method.split_once('.')
179                && allowed_namespaces.contains(ns)
180            {
181                continue;
182            }
183            errors.push(TypeError {
184                message: format!(
185                    "module '{}' declared `effects [{}]` but '{}' uses '{}' which is not in the declared boundary",
186                    module.name,
187                    allowed_list.join(", "),
188                    fd.name,
189                    method
190                ),
191                line: eff.line,
192                col: 1,
193                secondary: module.effects_line.map(|l| TypeErrorSpan {
194                    line: l,
195                    col: 1,
196                    label: "module effects declared here".to_string(),
197                }),
198            });
199        }
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Internal structures
205// ---------------------------------------------------------------------------
206
207#[derive(Debug, Clone)]
208struct FnSig {
209    params: Vec<Type>,
210    ret: Type,
211    effects: Vec<String>,
212}
213
214struct TypeChecker {
215    fn_sigs: HashMap<String, FnSig>,
216    value_members: HashMap<String, Type>,
217    /// Field types for record types: "TypeName.fieldName" → Type.
218    /// Populated for both user-defined `record` types and built-in records
219    /// (HttpResponse, Header). Enables checked dot-access on Named types.
220    record_field_types: HashMap<String, Type>,
221    /// Unqualified → qualified aliases for cross-module lookups.
222    /// E.g. "Shape.Circle" → "Data.Shape.Circle".
223    sig_aliases: HashMap<String, String>,
224    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
225    /// Pre-populated for Result and Option; extended by user-defined sum types.
226    type_variants: HashMap<String, Vec<String>>,
227    /// Top-level bindings visible from function bodies.
228    globals: HashMap<String, Type>,
229    /// Local bindings in the current function/scope.
230    locals: HashMap<String, Type>,
231    errors: Vec<TypeError>,
232    /// Return type of the function currently being checked; None at top level.
233    current_fn_ret: Option<Type>,
234    /// Line number of the function currently being checked; None at top level.
235    current_fn_line: Option<usize>,
236    /// Type names that are opaque in this module's context (imported via `exposes opaque`).
237    opaque_types: HashSet<String>,
238    /// When `true`, opaque-type construction + field-access + pattern-match
239    /// checks are bypassed. Used only by the self-host compile path
240    /// (`aver compile --with-self-host-support`) where
241    /// `self_hosted/domain/builtins.av` round-trips opaque host types
242    /// (e.g. `Tcp.Connection`) through the replay `Val` representation:
243    /// it serialises by reading `.id` / `.host` / `.port`, and
244    /// reconstructs by `Tcp.Connection(id = …, host = …, port = …)` on
245    /// replay deserialise. Both operations are forbidden in user code by
246    /// design (Phase 4.7+ fix #11), but the self-host has to read +
247    /// write the underlying record shape because that's the contract
248    /// with the replay JSON format. The flag is set by
249    /// [`run_type_check_full_self_host`] / [`run_type_check_with_loaded_self_host`]
250    /// and never user-toggleable from source.
251    self_host_mode: bool,
252    /// Names referenced during type checking of current function body (for unused detection).
253    used_names: HashSet<String>,
254    /// Bindings defined in the current function body: (name, line).
255    fn_bindings: Vec<(String, usize)>,
256    /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
257    unused_warnings: Vec<(String, String, usize)>,
258    /// Oracle v1: `.result` / `.trace` / `.trace.*` projections are
259    /// only meaningful inside `verify <fn> trace` cases. This flag is
260    /// set true while checking such a case's LHS / RHS, false
261    /// otherwise. Outside verify-trace the projections are rejected at
262    /// check time — otherwise user code would type-check then crash
263    /// at runtime with "namespace has no member 'trace'".
264    in_verify_trace_context: bool,
265}
266
267impl TypeChecker {
268    fn new() -> Self {
269        let mut type_variants = HashMap::new();
270        type_variants.insert(
271            "Result".to_string(),
272            vec!["Ok".to_string(), "Err".to_string()],
273        );
274        type_variants.insert(
275            "Option".to_string(),
276            vec!["Some".to_string(), "None".to_string()],
277        );
278
279        let mut tc = TypeChecker {
280            fn_sigs: HashMap::new(),
281            value_members: HashMap::new(),
282            record_field_types: HashMap::new(),
283            sig_aliases: HashMap::new(),
284            type_variants,
285            globals: HashMap::new(),
286            locals: HashMap::new(),
287            errors: Vec::new(),
288            current_fn_ret: None,
289            current_fn_line: None,
290            opaque_types: HashSet::new(),
291            self_host_mode: false,
292            used_names: HashSet::new(),
293            fn_bindings: Vec::new(),
294            unused_warnings: Vec::new(),
295            in_verify_trace_context: false,
296        };
297        tc.register_builtins();
298        tc
299    }
300
301    // -- Alias-aware lookups ------------------------------------------------
302
303    fn find_fn_sig(&self, key: &str) -> Option<&FnSig> {
304        self.fn_sigs
305            .get(key)
306            .or_else(|| self.sig_aliases.get(key).and_then(|c| self.fn_sigs.get(c)))
307    }
308
309    fn find_value_member(&self, key: &str) -> Option<&Type> {
310        self.value_members.get(key).or_else(|| {
311            self.sig_aliases
312                .get(key)
313                .and_then(|c| self.value_members.get(c))
314        })
315    }
316
317    fn find_record_field_type(&self, key: &str) -> Option<&Type> {
318        self.record_field_types.get(key).or_else(|| {
319            self.sig_aliases
320                .get(key)
321                .and_then(|c| self.record_field_types.get(c))
322        })
323    }
324
325    // -- Helpers -----------------------------------------------------------
326
327    /// Check whether `required_effect` is satisfied by `caller_effects`.
328    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
329        caller_effects
330            .iter()
331            .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
332    }
333
334    fn error(&mut self, msg: impl Into<String>) {
335        let line = self.current_fn_line.unwrap_or(1);
336        self.errors.push(TypeError {
337            message: msg.into(),
338            line,
339            col: 0,
340            secondary: None,
341        });
342    }
343
344    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
345        self.errors.push(TypeError {
346            message: msg.into(),
347            line,
348            col: 0,
349            secondary: None,
350        });
351    }
352
353    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
354        self.fn_sigs.insert(
355            name.to_string(),
356            FnSig {
357                params: params.to_vec(),
358                ret,
359                effects: effects.iter().map(|s| s.to_string()).collect(),
360            },
361        );
362    }
363
364    fn fn_type_from_sig(sig: &FnSig) -> Type {
365        Type::Fn(
366            sig.params.clone(),
367            Box::new(sig.ret.clone()),
368            sig.effects.clone(),
369        )
370    }
371
372    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
373        match ty {
374            Type::Fn(params, ret, effects) => Some(FnSig {
375                params: params.clone(),
376                ret: *ret.clone(),
377                effects: effects.clone(),
378            }),
379            _ => None,
380        }
381    }
382
383    fn binding_type(&self, name: &str) -> Option<Type> {
384        self.locals
385            .get(name)
386            .or_else(|| self.globals.get(name))
387            .cloned()
388    }
389
390    /// Compatibility used for checker constraints (returns, ascriptions, and
391    /// simple non-polymorphic call args). Variables in the expected type may bind
392    /// to the actual type; variables in the actual type only satisfy the exact
393    /// same expected variable and never satisfy concrete requirements.
394    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
395        let mut subst = HashMap::new();
396        Self::match_expected_type(actual, expected, &mut subst)
397    }
398
399    pub(super) fn match_expected_type(
400        actual: &Type,
401        expected: &Type,
402        subst: &mut HashMap<String, Type>,
403    ) -> bool {
404        match expected {
405            Type::Var(name) => Self::bind_expected_var(name, actual, subst),
406            Type::Invalid => false,
407            Type::Int => matches!(actual, Type::Int),
408            Type::Float => matches!(actual, Type::Float),
409            Type::Str => matches!(actual, Type::Str),
410            Type::Bool => matches!(actual, Type::Bool),
411            Type::Unit => matches!(actual, Type::Unit),
412            Type::Named(expected_name) => match actual {
413                Type::Named(actual_name) => {
414                    actual_name == expected_name
415                        || actual_name.ends_with(&format!(".{}", expected_name))
416                        || expected_name.ends_with(&format!(".{}", actual_name))
417                }
418                _ => false,
419            },
420            Type::Option(expected_inner) => match actual {
421                Type::Option(actual_inner) => {
422                    Self::match_expected_type(actual_inner, expected_inner, subst)
423                }
424                _ => false,
425            },
426            Type::List(expected_inner) => match actual {
427                Type::List(actual_inner) => {
428                    Self::match_expected_type(actual_inner, expected_inner, subst)
429                }
430                _ => false,
431            },
432            Type::Vector(expected_inner) => match actual {
433                Type::Vector(actual_inner) => {
434                    Self::match_expected_type(actual_inner, expected_inner, subst)
435                }
436                _ => false,
437            },
438            Type::Result(expected_ok, expected_err) => match actual {
439                Type::Result(actual_ok, actual_err) => {
440                    Self::match_expected_type(actual_ok, expected_ok, subst)
441                        && Self::match_expected_type(actual_err, expected_err, subst)
442                }
443                _ => false,
444            },
445            Type::Map(expected_k, expected_v) => match actual {
446                Type::Map(actual_k, actual_v) => {
447                    Self::match_expected_type(actual_k, expected_k, subst)
448                        && Self::match_expected_type(actual_v, expected_v, subst)
449                }
450                _ => false,
451            },
452            Type::Tuple(expected_items) => match actual {
453                Type::Tuple(actual_items) if actual_items.len() == expected_items.len() => {
454                    actual_items.iter().zip(expected_items.iter()).all(
455                        |(actual_item, expected_item)| {
456                            Self::match_expected_type(actual_item, expected_item, subst)
457                        },
458                    )
459                }
460                _ => false,
461            },
462            Type::Fn(expected_params, expected_ret, expected_effects) => match actual {
463                Type::Fn(actual_params, actual_ret, actual_effects)
464                    if actual_params.len() == expected_params.len() =>
465                {
466                    actual_params.iter().zip(expected_params.iter()).all(
467                        |(actual_param, expected_param)| {
468                            Self::match_expected_type(actual_param, expected_param, subst)
469                        },
470                    ) && Self::match_expected_type(actual_ret, expected_ret, subst)
471                        && actual_effects.iter().all(|actual| {
472                            expected_effects
473                                .iter()
474                                .any(|expected| crate::effects::effect_satisfies(expected, actual))
475                        })
476                }
477                _ => false,
478            },
479        }
480    }
481
482    fn bind_expected_var(name: &str, actual: &Type, subst: &mut HashMap<String, Type>) -> bool {
483        match actual {
484            Type::Var(actual_name) => return actual_name == name,
485            Type::Invalid => return false,
486            _ => {}
487        }
488        if let Some(bound) = subst.get(name).cloned() {
489            return Self::match_expected_type(actual, &bound, subst)
490                && Self::match_expected_type(&bound, actual, subst);
491        }
492        subst.insert(name.to_string(), actual.clone());
493        true
494    }
495
496    pub(super) fn instantiate_type(ty: &Type, subst: &HashMap<String, Type>) -> Type {
497        match ty {
498            Type::Var(name) => subst.get(name).cloned().unwrap_or_else(|| ty.clone()),
499            Type::Result(ok, err) => Type::Result(
500                Box::new(Self::instantiate_type(ok, subst)),
501                Box::new(Self::instantiate_type(err, subst)),
502            ),
503            Type::Option(inner) => Type::Option(Box::new(Self::instantiate_type(inner, subst))),
504            Type::List(inner) => Type::List(Box::new(Self::instantiate_type(inner, subst))),
505            Type::Vector(inner) => Type::Vector(Box::new(Self::instantiate_type(inner, subst))),
506            Type::Map(k, v) => Type::Map(
507                Box::new(Self::instantiate_type(k, subst)),
508                Box::new(Self::instantiate_type(v, subst)),
509            ),
510            Type::Tuple(items) => Type::Tuple(
511                items
512                    .iter()
513                    .map(|item| Self::instantiate_type(item, subst))
514                    .collect(),
515            ),
516            Type::Fn(params, ret, effects) => Type::Fn(
517                params
518                    .iter()
519                    .map(|param| Self::instantiate_type(param, subst))
520                    .collect(),
521                Box::new(Self::instantiate_type(ret, subst)),
522                effects.clone(),
523            ),
524            Type::Int
525            | Type::Float
526            | Type::Str
527            | Type::Bool
528            | Type::Unit
529            | Type::Invalid
530            | Type::Named(_) => ty.clone(),
531        }
532    }
533}