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;
19pub mod effect_classification;
20pub mod effect_lifting;
21mod exhaustiveness;
22mod flow;
23mod infer;
24mod memo;
25mod modules;
26pub mod proof_trust_header;
27
28#[cfg(test)]
29mod tests;
30
31// ---------------------------------------------------------------------------
32// Public API
33// ---------------------------------------------------------------------------
34
35#[derive(Debug, Clone)]
36pub struct TypeError {
37    pub message: String,
38    pub line: usize,
39    pub col: usize,
40    /// Optional secondary span for multi-region diagnostics (e.g. declared type vs actual return).
41    pub secondary: Option<TypeErrorSpan>,
42}
43
44#[derive(Debug, Clone)]
45pub struct TypeErrorSpan {
46    pub line: usize,
47    pub col: usize,
48    pub label: String,
49}
50
51/// Result of type-checking that also carries memo-safety metadata.
52#[derive(Debug)]
53pub struct TypeCheckResult {
54    pub errors: Vec<TypeError>,
55    /// For each user-defined fn: (param_types, return_type, effects).
56    /// Used by the memo system to decide which fns qualify.
57    pub fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)>,
58    /// Set of type names whose values are memo-safe (hashable scalars / records of scalars).
59    pub memo_safe_types: HashSet<String>,
60    /// Unused binding warnings: (binding_name, fn_name, line).
61    pub unused_bindings: Vec<(String, String, usize)>,
62}
63
64pub fn run_type_check(items: &[TopLevel]) -> Vec<TypeError> {
65    run_type_check_with_base(items, None)
66}
67
68pub fn run_type_check_with_base(items: &[TopLevel], base_dir: Option<&str>) -> Vec<TypeError> {
69    run_type_check_full(items, base_dir).errors
70}
71
72pub fn run_type_check_full(items: &[TopLevel], base_dir: Option<&str>) -> TypeCheckResult {
73    let mut checker = TypeChecker::new();
74    checker.check(items, base_dir);
75    finalize_check_result(checker, items)
76}
77
78/// Variant of [`run_type_check_full`] that uses pre-loaded dependency
79/// modules instead of resolving them from disk. The playground feeds
80/// this from its in-memory virtual fs so multi-file projects type-
81/// check without any filesystem access.
82pub fn run_type_check_with_loaded(
83    items: &[TopLevel],
84    loaded: &[crate::source::LoadedModule],
85) -> TypeCheckResult {
86    let mut checker = TypeChecker::new();
87    checker.check_with_loaded(items, loaded);
88    finalize_check_result(checker, items)
89}
90
91fn finalize_check_result(checker: TypeChecker, items: &[TopLevel]) -> TypeCheckResult {
92    let fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = checker
93        .fn_sigs
94        .iter()
95        .map(|(k, v)| {
96            (
97                k.clone(),
98                (v.params.clone(), v.ret.clone(), v.effects.clone()),
99            )
100        })
101        .collect();
102
103    let memo_safe_types = checker.compute_memo_safe_types(items);
104
105    TypeCheckResult {
106        errors: checker.errors,
107        fn_sigs,
108        memo_safe_types,
109        unused_bindings: checker.unused_warnings,
110    }
111}
112
113// ---------------------------------------------------------------------------
114// Internal structures
115// ---------------------------------------------------------------------------
116
117#[derive(Debug, Clone)]
118struct FnSig {
119    params: Vec<Type>,
120    ret: Type,
121    effects: Vec<String>,
122}
123
124struct TypeChecker {
125    fn_sigs: HashMap<String, FnSig>,
126    value_members: HashMap<String, Type>,
127    /// Field types for record types: "TypeName.fieldName" → Type.
128    /// Populated for both user-defined `record` types and built-in records
129    /// (HttpResponse, Header). Enables checked dot-access on Named types.
130    record_field_types: HashMap<String, Type>,
131    /// Unqualified → qualified aliases for cross-module lookups.
132    /// E.g. "Shape.Circle" → "Data.Shape.Circle".
133    sig_aliases: HashMap<String, String>,
134    /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
135    /// Pre-populated for Result and Option; extended by user-defined sum types.
136    type_variants: HashMap<String, Vec<String>>,
137    /// Top-level bindings visible from function bodies.
138    globals: HashMap<String, Type>,
139    /// Local bindings in the current function/scope.
140    locals: HashMap<String, Type>,
141    errors: Vec<TypeError>,
142    /// Return type of the function currently being checked; None at top level.
143    current_fn_ret: Option<Type>,
144    /// Line number of the function currently being checked; None at top level.
145    current_fn_line: Option<usize>,
146    /// Type names that are opaque in this module's context (imported via `exposes opaque`).
147    opaque_types: HashSet<String>,
148    /// Names referenced during type checking of current function body (for unused detection).
149    used_names: HashSet<String>,
150    /// Bindings defined in the current function body: (name, line).
151    fn_bindings: Vec<(String, usize)>,
152    /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
153    unused_warnings: Vec<(String, String, usize)>,
154    /// Oracle v1: `.result` / `.trace` / `.trace.*` projections are
155    /// only meaningful inside `verify <fn> trace` cases. This flag is
156    /// set true while checking such a case's LHS / RHS, false
157    /// otherwise. Outside verify-trace the projections are rejected at
158    /// check time — otherwise user code would type-check then crash
159    /// at runtime with "namespace has no member 'trace'".
160    in_verify_trace_context: bool,
161}
162
163impl TypeChecker {
164    fn new() -> Self {
165        let mut type_variants = HashMap::new();
166        type_variants.insert(
167            "Result".to_string(),
168            vec!["Ok".to_string(), "Err".to_string()],
169        );
170        type_variants.insert(
171            "Option".to_string(),
172            vec!["Some".to_string(), "None".to_string()],
173        );
174
175        let mut tc = TypeChecker {
176            fn_sigs: HashMap::new(),
177            value_members: HashMap::new(),
178            record_field_types: HashMap::new(),
179            sig_aliases: HashMap::new(),
180            type_variants,
181            globals: HashMap::new(),
182            locals: HashMap::new(),
183            errors: Vec::new(),
184            current_fn_ret: None,
185            current_fn_line: None,
186            opaque_types: HashSet::new(),
187            used_names: HashSet::new(),
188            fn_bindings: Vec::new(),
189            unused_warnings: Vec::new(),
190            in_verify_trace_context: false,
191        };
192        tc.register_builtins();
193        tc
194    }
195
196    // -- Alias-aware lookups ------------------------------------------------
197
198    fn find_fn_sig(&self, key: &str) -> Option<&FnSig> {
199        self.fn_sigs
200            .get(key)
201            .or_else(|| self.sig_aliases.get(key).and_then(|c| self.fn_sigs.get(c)))
202    }
203
204    fn find_value_member(&self, key: &str) -> Option<&Type> {
205        self.value_members.get(key).or_else(|| {
206            self.sig_aliases
207                .get(key)
208                .and_then(|c| self.value_members.get(c))
209        })
210    }
211
212    fn find_record_field_type(&self, key: &str) -> Option<&Type> {
213        self.record_field_types.get(key).or_else(|| {
214            self.sig_aliases
215                .get(key)
216                .and_then(|c| self.record_field_types.get(c))
217        })
218    }
219
220    // -- Helpers -----------------------------------------------------------
221
222    /// Check whether `required_effect` is satisfied by `caller_effects`.
223    fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
224        caller_effects
225            .iter()
226            .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
227    }
228
229    fn error(&mut self, msg: impl Into<String>) {
230        let line = self.current_fn_line.unwrap_or(1);
231        self.errors.push(TypeError {
232            message: msg.into(),
233            line,
234            col: 0,
235            secondary: None,
236        });
237    }
238
239    fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
240        self.errors.push(TypeError {
241            message: msg.into(),
242            line,
243            col: 0,
244            secondary: None,
245        });
246    }
247
248    fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
249        self.fn_sigs.insert(
250            name.to_string(),
251            FnSig {
252                params: params.to_vec(),
253                ret,
254                effects: effects.iter().map(|s| s.to_string()).collect(),
255            },
256        );
257    }
258
259    fn fn_type_from_sig(sig: &FnSig) -> Type {
260        Type::Fn(
261            sig.params.clone(),
262            Box::new(sig.ret.clone()),
263            sig.effects.clone(),
264        )
265    }
266
267    fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
268        match ty {
269            Type::Fn(params, ret, effects) => Some(FnSig {
270                params: params.clone(),
271                ret: *ret.clone(),
272                effects: effects.clone(),
273            }),
274            _ => None,
275        }
276    }
277
278    fn binding_type(&self, name: &str) -> Option<Type> {
279        self.locals
280            .get(name)
281            .or_else(|| self.globals.get(name))
282            .cloned()
283    }
284
285    /// Compatibility used for checker constraints (call args, returns, ascriptions).
286    ///
287    /// We keep gradual typing for nested placeholders (`Result<Int, Unknown>` can
288    /// still fit `Result<Int, String>`), but reject *bare* `Unknown` when a
289    /// concrete type is required. This closes common false negatives where an
290    /// unresolved expression silently passes a concrete signature.
291    pub(super) fn constraint_compatible(actual: &Type, expected: &Type) -> bool {
292        if matches!(actual, Type::Unknown) && !matches!(expected, Type::Unknown) {
293            return false;
294        }
295        actual.compatible(expected)
296    }
297}