Skip to main content

lumen_compiler/compiler/
typecheck.rs

1//! Bidirectional type inference and checking for Lumen.
2
3use crate::compiler::ast::*;
4use crate::compiler::resolve::SymbolTable;
5
6use std::collections::HashMap;
7use thiserror::Error;
8
9/// Check if a name is a built-in function
10fn is_builtin_function(name: &str) -> bool {
11    matches!(
12        name,
13        "print"
14            | "len"
15            | "length"
16            | "append"
17            | "range"
18            | "to_string"
19            | "str"
20            | "to_int"
21            | "int"
22            | "to_float"
23            | "float"
24            | "type_of"
25            | "keys"
26            | "values"
27            | "contains"
28            | "join"
29            | "split"
30            | "trim"
31            | "upper"
32            | "lower"
33            | "replace"
34            | "abs"
35            | "min"
36            | "max"
37            | "hash"
38            | "not"
39            | "count"
40            | "matches"
41            | "slice"
42            | "sort"
43            | "reverse"
44            | "map"
45            | "filter"
46            | "reduce"
47            | "parallel"
48            | "race"
49            | "vote"
50            | "select"
51            | "timeout"
52            | "spawn"
53            | "resume"
54    )
55}
56
57/// In doc_mode / non-strict mode, allow undefined variable names that look like
58/// plausible identifiers (e.g. doc snippet references). Returns false for names
59/// that are likely typos or real errors even in non-strict mode.
60fn is_doc_placeholder_var(name: &str) -> bool {
61    // All well-formed identifiers are allowed in placeholder mode.
62    // The guard (`allow_placeholders`) is only true in doc_mode or non-strict mode.
63    !name.is_empty() && !name.starts_with("__")
64}
65
66fn desugar_pipe_application(
67    input: &Expr,
68    stage: &Expr,
69    span: crate::compiler::tokens::Span,
70) -> Expr {
71    match stage {
72        Expr::Call(callee, args, call_span) => {
73            let mut call_args = Vec::with_capacity(args.len() + 1);
74            call_args.push(CallArg::Positional(input.clone()));
75            call_args.extend(args.clone());
76            Expr::Call(callee.clone(), call_args, *call_span)
77        }
78        Expr::ToolCall(callee, args, call_span) => {
79            let mut call_args = Vec::with_capacity(args.len() + 1);
80            call_args.push(CallArg::Positional(input.clone()));
81            call_args.extend(args.clone());
82            Expr::ToolCall(callee.clone(), call_args, *call_span)
83        }
84        _ => Expr::Call(
85            Box::new(stage.clone()),
86            vec![CallArg::Positional(input.clone())],
87            span,
88        ),
89    }
90}
91
92fn type_contains_any(ty: &Type) -> bool {
93    match ty {
94        Type::Any => true,
95        Type::List(inner) | Type::Set(inner) => type_contains_any(inner),
96        Type::Map(k, v) | Type::Result(k, v) => type_contains_any(k) || type_contains_any(v),
97        Type::Fn(params, ret) => params.iter().any(type_contains_any) || type_contains_any(ret),
98        Type::Union(types) | Type::Tuple(types) => types.iter().any(type_contains_any),
99        Type::TypeRef(_, args) => args.iter().any(type_contains_any),
100        _ => false,
101    }
102}
103
104/// Compute Levenshtein edit distance between two strings
105fn edit_distance(a: &str, b: &str) -> usize {
106    let a_chars: Vec<char> = a.chars().collect();
107    let b_chars: Vec<char> = b.chars().collect();
108    let a_len = a_chars.len();
109    let b_len = b_chars.len();
110
111    if a_len == 0 {
112        return b_len;
113    }
114    if b_len == 0 {
115        return a_len;
116    }
117
118    let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
119
120    for (i, row) in matrix.iter_mut().enumerate() {
121        row[0] = i;
122    }
123    #[allow(clippy::needless_range_loop)]
124    for j in 0..=b_len {
125        matrix[0][j] = j;
126    }
127
128    for i in 1..=a_len {
129        for j in 1..=b_len {
130            let cost = if a_chars[i - 1] == b_chars[j - 1] {
131                0
132            } else {
133                1
134            };
135            matrix[i][j] = (matrix[i - 1][j] + 1)
136                .min(matrix[i][j - 1] + 1)
137                .min(matrix[i - 1][j - 1] + cost);
138        }
139    }
140
141    matrix[a_len][b_len]
142}
143
144/// Find similar names for "did you mean?" suggestions
145fn suggest_similar(name: &str, candidates: &[&str], max_distance: usize) -> Vec<String> {
146    let mut matches: Vec<(usize, String)> = candidates
147        .iter()
148        .filter_map(|c| {
149            let d = edit_distance(name, c);
150            if d <= max_distance && d < name.len() {
151                Some((d, c.to_string()))
152            } else {
153                None
154            }
155        })
156        .collect();
157
158    matches.sort_by_key(|(d, _)| *d);
159    matches.into_iter().map(|(_, s)| s).take(3).collect()
160}
161
162#[derive(Debug, Error)]
163pub enum TypeError {
164    #[error("type mismatch at line {line}: expected {expected}, got {actual}")]
165    Mismatch {
166        expected: String,
167        actual: String,
168        line: usize,
169    },
170    #[error("undefined variable '{name}' at line {line}")]
171    UndefinedVar { name: String, line: usize },
172    #[error("not callable at line {line}")]
173    NotCallable { line: usize },
174    #[error("wrong number of arguments at line {line}: expected {expected}, got {actual}")]
175    ArgCount {
176        expected: usize,
177        actual: usize,
178        line: usize,
179    },
180    #[error("unknown field '{field}' on type '{ty}' at line {line}")]
181    UnknownField {
182        field: String,
183        ty: String,
184        line: usize,
185        suggestions: Vec<String>,
186    },
187    #[error("undefined type '{name}' at line {line}")]
188    UndefinedType { name: String, line: usize },
189    #[error("missing return in cell '{name}' at line {line}")]
190    MissingReturn { name: String, line: usize },
191    #[error("cannot assign to immutable variable '{name}' at line {line}")]
192    ImmutableAssign { name: String, line: usize },
193    #[error("incomplete match at line {line}: missing variants {missing:?}")]
194    IncompleteMatch {
195        enum_name: String,
196        missing: Vec<String>,
197        line: usize,
198    },
199}
200
201/// Resolved type representation
202#[derive(Debug, Clone, PartialEq)]
203pub enum Type {
204    String,
205    Int,
206    Float,
207    Bool,
208    Bytes,
209    Json,
210    Null,
211    List(Box<Type>),
212    Map(Box<Type>, Box<Type>),
213    Record(String),
214    Enum(String),
215    Result(Box<Type>, Box<Type>),
216    Union(Vec<Type>),
217    Tuple(Vec<Type>),
218    Set(Box<Type>),
219    Fn(Vec<Type>, Box<Type>),
220    Generic(String),
221    TypeRef(String, Vec<Type>),
222    Any, // For unresolved / error recovery
223}
224
225impl std::fmt::Display for Type {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        match self {
228            Type::String => write!(f, "String"),
229            Type::Int => write!(f, "Int"),
230            Type::Float => write!(f, "Float"),
231            Type::Bool => write!(f, "Bool"),
232            Type::Bytes => write!(f, "Bytes"),
233            Type::Json => write!(f, "Json"),
234            Type::Null => write!(f, "Null"),
235            Type::Any => write!(f, "Any"),
236            Type::List(t) => write!(f, "list[{}]", t),
237            Type::Map(k, v) => write!(f, "map[{}, {}]", k, v),
238            Type::Record(n) => write!(f, "{}", n),
239            Type::Enum(n) => write!(f, "{}", n),
240            Type::Result(o, e) => write!(f, "result[{}, {}]", o, e),
241            Type::Union(ts) => {
242                let parts: Vec<_> = ts.iter().map(|t| format!("{}", t)).collect();
243                write!(f, "{}", parts.join(" | "))
244            }
245            Type::Tuple(ts) => {
246                let parts: Vec<_> = ts.iter().map(|t| format!("{}", t)).collect();
247                write!(f, "({})", parts.join(", "))
248            }
249            Type::Set(t) => write!(f, "set[{}]", t),
250            Type::Fn(params, ret) => {
251                let ps: Vec<_> = params.iter().map(|t| format!("{}", t)).collect();
252                write!(f, "fn({}) -> {}", ps.join(", "), ret)
253            }
254            Type::Generic(n) => write!(f, "{}", n),
255            Type::TypeRef(n, args) => {
256                let as_: Vec<_> = args.iter().map(|t| format!("{}", t)).collect();
257                write!(f, "{}[{}]", n, as_.join(", "))
258            }
259        }
260    }
261}
262
263/// Type substitution map: maps type parameter names to concrete types
264pub type TypeSubst = HashMap<String, Type>;
265
266/// Build substitution map from generic parameters and concrete type arguments
267fn build_subst(params: &[String], args: &[Type]) -> TypeSubst {
268    params
269        .iter()
270        .zip(args.iter())
271        .map(|(p, a)| (p.clone(), a.clone()))
272        .collect()
273}
274
275/// Infer generic type arguments from record field values
276fn infer_generic_args_from_fields(
277    generic_params: &[String],
278    field_defs: &[FieldDef],
279    field_values: &[(String, Expr)],
280    symbols: &SymbolTable,
281    checker: &mut TypeChecker,
282) -> Vec<Type> {
283    let mut inferred: HashMap<String, Type> = HashMap::new();
284
285    for (fname, fval) in field_values {
286        let val_type = checker.infer_expr(fval);
287        if let Some(field_def) = field_defs.iter().find(|f| f.name == *fname) {
288            // Try to unify field type with value type to infer generic params
289            unify_for_inference(&field_def.ty, &val_type, symbols, &mut inferred);
290        }
291    }
292
293    // Return inferred types in parameter order, or Type::Any for unresolved
294    generic_params
295        .iter()
296        .map(|p| inferred.get(p).cloned().unwrap_or(Type::Any))
297        .collect()
298}
299
300/// Attempt to unify a type expression with a concrete type to infer generic parameters
301fn unify_for_inference(
302    type_expr: &TypeExpr,
303    concrete: &Type,
304    _symbols: &SymbolTable,
305    inferred: &mut HashMap<String, Type>,
306) {
307    match (type_expr, concrete) {
308        (TypeExpr::Named(name, _), ty) => {
309            // If this is a type parameter (single uppercase letter or explicitly generic),
310            // record the inference
311            if name.len() == 1 && name.chars().next().unwrap().is_uppercase() {
312                inferred.entry(name.clone()).or_insert_with(|| ty.clone());
313            }
314        }
315        (TypeExpr::List(inner, _), Type::List(inner_ty)) => {
316            unify_for_inference(inner, inner_ty, _symbols, inferred);
317        }
318        (TypeExpr::Map(k, v, _), Type::Map(kt, vt)) => {
319            unify_for_inference(k, kt, _symbols, inferred);
320            unify_for_inference(v, vt, _symbols, inferred);
321        }
322        (TypeExpr::Set(inner, _), Type::Set(inner_ty)) => {
323            unify_for_inference(inner, inner_ty, _symbols, inferred);
324        }
325        (TypeExpr::Result(ok, err, _), Type::Result(ok_ty, err_ty)) => {
326            unify_for_inference(ok, ok_ty, _symbols, inferred);
327            unify_for_inference(err, err_ty, _symbols, inferred);
328        }
329        (TypeExpr::Tuple(exprs, _), Type::Tuple(types)) => {
330            for (expr, ty) in exprs.iter().zip(types.iter()) {
331                unify_for_inference(expr, ty, _symbols, inferred);
332            }
333        }
334        _ => {
335            // No unification possible
336        }
337    }
338}
339
340/// Resolve a type expression to a concrete type, applying substitutions for generic parameters
341pub fn resolve_type_expr(ty: &TypeExpr, symbols: &SymbolTable) -> Type {
342    resolve_type_expr_with_subst(ty, symbols, &HashMap::new())
343}
344
345/// Resolve a type expression with generic type parameter substitutions
346fn resolve_type_expr_with_subst(ty: &TypeExpr, symbols: &SymbolTable, subst: &TypeSubst) -> Type {
347    match ty {
348        TypeExpr::Named(name, _) => {
349            // Check if this is a generic type parameter
350            if let Some(concrete_type) = subst.get(name) {
351                return concrete_type.clone();
352            }
353            match name.as_str() {
354                "String" => Type::String,
355                "Int" => Type::Int,
356                "Float" => Type::Float,
357                "Bool" => Type::Bool,
358                "Bytes" => Type::Bytes,
359                "Json" => Type::Json,
360                _ => {
361                    if symbols.types.contains_key(name) {
362                        use crate::compiler::resolve::TypeInfoKind;
363                        match &symbols.types[name].kind {
364                            TypeInfoKind::Record(_) => Type::Record(name.clone()),
365                            TypeInfoKind::Enum(_) => Type::Enum(name.clone()),
366                            TypeInfoKind::Builtin => Type::Record(name.clone()),
367                        }
368                    } else if let Some(alias_target) = symbols.type_aliases.get(name) {
369                        resolve_type_expr_with_subst(alias_target, symbols, subst)
370                    } else {
371                        Type::Any
372                    }
373                }
374            }
375        }
376        TypeExpr::List(inner, _) => Type::List(Box::new(resolve_type_expr_with_subst(
377            inner, symbols, subst,
378        ))),
379        TypeExpr::Map(k, v, _) => Type::Map(
380            Box::new(resolve_type_expr_with_subst(k, symbols, subst)),
381            Box::new(resolve_type_expr_with_subst(v, symbols, subst)),
382        ),
383        TypeExpr::Result(ok, err, _) => Type::Result(
384            Box::new(resolve_type_expr_with_subst(ok, symbols, subst)),
385            Box::new(resolve_type_expr_with_subst(err, symbols, subst)),
386        ),
387        TypeExpr::Union(types, _) => Type::Union(
388            types
389                .iter()
390                .map(|t| resolve_type_expr_with_subst(t, symbols, subst))
391                .collect(),
392        ),
393        TypeExpr::Null(_) => Type::Null,
394        TypeExpr::Tuple(types, _) => Type::Tuple(
395            types
396                .iter()
397                .map(|t| resolve_type_expr_with_subst(t, symbols, subst))
398                .collect(),
399        ),
400        TypeExpr::Set(inner, _) => Type::Set(Box::new(resolve_type_expr_with_subst(
401            inner, symbols, subst,
402        ))),
403        TypeExpr::Fn(params, ret, _, _) => {
404            let param_types = params
405                .iter()
406                .map(|t| resolve_type_expr_with_subst(t, symbols, subst))
407                .collect();
408            let ret_type = resolve_type_expr_with_subst(ret, symbols, subst);
409            Type::Fn(param_types, Box::new(ret_type))
410        }
411        TypeExpr::Generic(name, args, _) => {
412            // Build substitution map for this generic instantiation
413            if let Some(type_info) = symbols.types.get(name) {
414                let generic_params = &type_info.generic_params;
415
416                // Resolve argument types with current substitution
417                let arg_types: Vec<_> = args
418                    .iter()
419                    .map(|t| resolve_type_expr_with_subst(t, symbols, subst))
420                    .collect();
421
422                // Create a new substitution map for the generic type's fields
423                if generic_params.len() == arg_types.len() {
424                    Type::TypeRef(name.clone(), arg_types)
425                } else {
426                    // Arity mismatch should have been caught in resolve phase
427                    Type::TypeRef(name.clone(), arg_types)
428                }
429            } else {
430                // Unknown type - should have been caught in resolve
431                let arg_types: Vec<_> = args
432                    .iter()
433                    .map(|t| resolve_type_expr_with_subst(t, symbols, subst))
434                    .collect();
435                Type::TypeRef(name.clone(), arg_types)
436            }
437        }
438    }
439}
440
441struct TypeChecker<'a> {
442    symbols: &'a SymbolTable,
443    allow_placeholders: bool,
444    locals: HashMap<String, Type>,
445    mutables: HashMap<String, bool>,
446    errors: Vec<TypeError>,
447}
448
449#[derive(Debug)]
450enum CheckedCallArg {
451    Positional(Type, usize),
452    Named(String, Type, usize),
453}
454
455impl<'a> TypeChecker<'a> {
456    fn new(symbols: &'a SymbolTable, allow_placeholders: bool) -> Self {
457        Self {
458            symbols,
459            allow_placeholders,
460            locals: HashMap::new(),
461            mutables: HashMap::new(),
462            errors: Vec::new(),
463        }
464    }
465
466    fn check_cell(&mut self, cell: &CellDef) {
467        self.locals.clear();
468        self.mutables.clear();
469        for p in &cell.params {
470            let ty = resolve_type_expr(&p.ty, self.symbols);
471            self.locals.insert(p.name.clone(), ty);
472            self.mutables.insert(p.name.clone(), true); // params are mutable by default
473        }
474        let return_type = if let Some(ref rt) = cell.return_type {
475            Some(resolve_type_expr(rt, self.symbols))
476        } else {
477            None
478        };
479
480        for stmt in &cell.body {
481            self.check_stmt(stmt, return_type.as_ref());
482        }
483    }
484
485    fn check_agent_cell(&mut self, cell: &CellDef) {
486        self.locals.clear();
487        self.mutables.clear();
488        self.locals.insert("self".into(), Type::Any);
489        self.mutables.insert("self".into(), true);
490        for p in &cell.params {
491            if p.name == "self" {
492                continue;
493            }
494            let ty = resolve_type_expr(&p.ty, self.symbols);
495            self.locals.insert(p.name.clone(), ty);
496            self.mutables.insert(p.name.clone(), true);
497        }
498        let return_type = if let Some(ref rt) = cell.return_type {
499            Some(resolve_type_expr(rt, self.symbols))
500        } else {
501            None
502        };
503        for stmt in &cell.body {
504            self.check_stmt(stmt, return_type.as_ref());
505        }
506    }
507
508    fn check_call_against_signature(
509        &mut self,
510        params: &[(String, TypeExpr)],
511        args: &[CheckedCallArg],
512        line: usize,
513    ) {
514        if args.len() > params.len() {
515            self.errors.push(TypeError::ArgCount {
516                expected: params.len(),
517                actual: args.len(),
518                line,
519            });
520        }
521
522        let mut positional_idx = 0usize;
523        for arg in args {
524            match arg {
525                CheckedCallArg::Positional(actual_ty, arg_line) => {
526                    if let Some((_, expected_expr)) = params.get(positional_idx) {
527                        let expected_ty = resolve_type_expr(expected_expr, self.symbols);
528                        self.check_compat(&expected_ty, actual_ty, *arg_line);
529                    }
530                    positional_idx += 1;
531                }
532                CheckedCallArg::Named(name, actual_ty, arg_line) => {
533                    if let Some((_, expected_expr)) = params.iter().find(|(p, _)| p == name) {
534                        let expected_ty = resolve_type_expr(expected_expr, self.symbols);
535                        self.check_compat(&expected_ty, actual_ty, *arg_line);
536                    } else {
537                        self.errors.push(TypeError::Mismatch {
538                            expected: format!("parameter '{}'", name),
539                            actual: "unknown named argument".to_string(),
540                            line: *arg_line,
541                        });
542                    }
543                }
544            }
545        }
546    }
547
548    fn check_stmt(&mut self, stmt: &Stmt, expected_return: Option<&Type>) {
549        match stmt {
550            Stmt::Let(ls) => {
551                let val_type = self.infer_expr(&ls.value);
552                if let Some(ref ann) = ls.ty {
553                    let expected = resolve_type_expr(ann, self.symbols);
554                    self.check_compat(&expected, &val_type, ls.span.line);
555                }
556                self.locals.insert(ls.name.clone(), val_type);
557                // In Lumen, all let bindings are reassignable by default
558                // `let mut` is just documentation; `const` is immutable
559                self.mutables.insert(ls.name.clone(), true);
560            }
561            Stmt::If(ifs) => {
562                let ct = self.infer_expr(&ifs.condition);
563                self.check_compat(&Type::Bool, &ct, ifs.span.line);
564                for s in &ifs.then_body {
565                    self.check_stmt(s, expected_return);
566                }
567                if let Some(ref eb) = ifs.else_body {
568                    for s in eb {
569                        self.check_stmt(s, expected_return);
570                    }
571                }
572            }
573            Stmt::For(fs) => {
574                let iter_type = self.infer_expr(&fs.iter);
575                let elem_type = match &iter_type {
576                    Type::List(inner) => *inner.clone(),
577                    Type::Set(inner) => *inner.clone(),
578                    Type::Map(k, _) => *k.clone(),
579                    Type::Any => Type::Any,
580                    _ => {
581                        self.errors.push(TypeError::Mismatch {
582                            expected: "iterable".into(),
583                            actual: format!("{}", iter_type),
584                            line: fs.span.line,
585                        });
586                        Type::Any
587                    }
588                };
589                self.locals.insert(fs.var.clone(), elem_type);
590                if let Some(filter) = &fs.filter {
591                    self.infer_expr(filter);
592                }
593                for s in &fs.body {
594                    self.check_stmt(s, expected_return);
595                }
596            }
597            Stmt::Match(ms) => {
598                let subject_type = self.infer_expr(&ms.subject);
599                let mut covered_variants = Vec::new();
600                let mut has_catchall = false;
601
602                for arm in &ms.arms {
603                    self.bind_match_pattern(
604                        &arm.pattern,
605                        &subject_type,
606                        &mut covered_variants,
607                        &mut has_catchall,
608                        arm.span.line,
609                    );
610                    for s in &arm.body {
611                        self.check_stmt(s, expected_return);
612                    }
613                }
614
615                // Exhaustiveness Check for Enums
616                if let Type::Enum(ref name) = subject_type {
617                    if !has_catchall {
618                        if let Some(ti) = self.symbols.types.get(name) {
619                            if let crate::compiler::resolve::TypeInfoKind::Enum(def) = &ti.kind {
620                                let missing: Vec<_> = def
621                                    .variants
622                                    .iter()
623                                    .filter(|v| !covered_variants.contains(&v.name))
624                                    .map(|v| v.name.clone())
625                                    .collect();
626                                if !missing.is_empty() {
627                                    self.errors.push(TypeError::IncompleteMatch {
628                                        enum_name: name.clone(),
629                                        missing,
630                                        line: ms.span.line,
631                                    });
632                                }
633                            }
634                        }
635                    }
636                }
637            }
638            Stmt::Return(rs) => {
639                let val_type = self.infer_expr(&rs.value);
640                if let Some(expected) = expected_return {
641                    self.check_compat(expected, &val_type, rs.span.line);
642                }
643            }
644            Stmt::Halt(hs) => {
645                self.infer_expr(&hs.message);
646            }
647            Stmt::Assign(asgn) => {
648                let val_type = self.infer_expr(&asgn.value);
649                // Check mutability
650                if let Some(&is_mut) = self.mutables.get(&asgn.target) {
651                    if !is_mut {
652                        self.errors.push(TypeError::ImmutableAssign {
653                            name: asgn.target.clone(),
654                            line: asgn.span.line,
655                        });
656                    }
657                }
658                self.locals.insert(asgn.target.clone(), val_type);
659            }
660            Stmt::Expr(es) => {
661                self.infer_expr(&es.expr);
662            }
663            Stmt::While(ws) => {
664                let ct = self.infer_expr(&ws.condition);
665                self.check_compat(&Type::Bool, &ct, ws.span.line);
666                for s in &ws.body {
667                    self.check_stmt(s, expected_return);
668                }
669            }
670            Stmt::Loop(ls) => {
671                for s in &ls.body {
672                    self.check_stmt(s, expected_return);
673                }
674            }
675            Stmt::Break(_) | Stmt::Continue(_) => {}
676            Stmt::Defer(ds) => {
677                for s in &ds.body {
678                    self.check_stmt(s, expected_return);
679                }
680            }
681            Stmt::Emit(es) => {
682                self.infer_expr(&es.value);
683            }
684            Stmt::CompoundAssign(ca) => {
685                let val_type = self.infer_expr(&ca.value);
686                // Check mutability
687                if let Some(&is_mut) = self.mutables.get(&ca.target) {
688                    if !is_mut {
689                        self.errors.push(TypeError::ImmutableAssign {
690                            name: ca.target.clone(),
691                            line: ca.span.line,
692                        });
693                    }
694                }
695                if let Some(existing) = self.locals.get(&ca.target).cloned() {
696                    // Bitwise compound assignments require Int operands
697                    match ca.op {
698                        CompoundOp::BitAndAssign
699                        | CompoundOp::BitOrAssign
700                        | CompoundOp::BitXorAssign => {
701                            if existing != Type::Any && existing != Type::Int {
702                                self.errors.push(TypeError::Mismatch {
703                                    expected: "Int".into(),
704                                    actual: format!("{}", existing),
705                                    line: ca.span.line,
706                                });
707                            }
708                            if val_type != Type::Any && val_type != Type::Int {
709                                self.errors.push(TypeError::Mismatch {
710                                    expected: "Int".into(),
711                                    actual: format!("{}", val_type),
712                                    line: ca.span.line,
713                                });
714                            }
715                        }
716                        _ => {
717                            self.check_compat(&existing, &val_type, ca.span.line);
718                        }
719                    }
720                }
721            }
722        }
723    }
724
725    fn bind_match_pattern(
726        &mut self,
727        pattern: &Pattern,
728        subject_type: &Type,
729        covered_variants: &mut Vec<String>,
730        has_catchall: &mut bool,
731        line: usize,
732    ) {
733        match pattern {
734            Pattern::Variant(tag, binding, _) => {
735                let mut valid_variant = false;
736                let mut payload_type = Type::Any;
737                let mut expects_payload = false;
738
739                if let Type::Enum(ref name) = subject_type {
740                    if let Some(ti) = self.symbols.types.get(name) {
741                        if let crate::compiler::resolve::TypeInfoKind::Enum(def) = &ti.kind {
742                            if let Some(variant) = def.variants.iter().find(|v| v.name == *tag) {
743                                valid_variant = true;
744                                covered_variants.push(tag.clone());
745                                if let Some(payload) = &variant.payload {
746                                    expects_payload = true;
747                                    payload_type = resolve_type_expr(payload, self.symbols);
748                                } else {
749                                    payload_type = Type::Null;
750                                }
751                            }
752                        }
753                    }
754                    if !valid_variant {
755                        self.errors.push(TypeError::Mismatch {
756                            expected: format!("variant of {}", name),
757                            actual: tag.clone(),
758                            line,
759                        });
760                    }
761                } else if let Type::Result(ref ok, ref err) = subject_type {
762                    if tag == "ok" {
763                        valid_variant = true;
764                        expects_payload = true;
765                        payload_type = *ok.clone();
766                    } else if tag == "err" {
767                        valid_variant = true;
768                        expects_payload = true;
769                        payload_type = *err.clone();
770                    }
771                    if !valid_variant {
772                        self.errors.push(TypeError::Mismatch {
773                            expected: "ok or err".into(),
774                            actual: tag.clone(),
775                            line,
776                        });
777                    }
778                }
779
780                if let Some(inner_pattern) = binding {
781                    if valid_variant && !expects_payload {
782                        self.errors.push(TypeError::Mismatch {
783                            expected: format!("{} without payload", tag),
784                            actual: format!("{}(...)", tag),
785                            line,
786                        });
787                    }
788                    self.bind_match_pattern(
789                        inner_pattern,
790                        &payload_type,
791                        covered_variants,
792                        has_catchall,
793                        line,
794                    );
795                }
796            }
797            Pattern::Ident(name, _) => {
798                // Check if this identifier is actually an enum variant without payload
799                let mut is_variant = false;
800                if let Type::Enum(ref enum_name) = subject_type {
801                    if let Some(ti) = self.symbols.types.get(enum_name) {
802                        if let crate::compiler::resolve::TypeInfoKind::Enum(def) = &ti.kind {
803                            if def.variants.iter().any(|v| v.name == *name) {
804                                // This is a variant pattern, not a binding
805                                covered_variants.push(name.clone());
806                                is_variant = true;
807                            }
808                        }
809                    }
810                }
811
812                if !is_variant {
813                    // Regular identifier pattern - binds the value
814                    self.locals.insert(name.clone(), subject_type.clone());
815                    *has_catchall = true;
816                }
817            }
818            Pattern::Wildcard(_) => {
819                *has_catchall = true;
820            }
821            Pattern::Guard {
822                inner, condition, ..
823            } => {
824                // Guarded arms are treated conservatively: the inner pattern
825                // binds variables but does NOT count toward exhaustiveness
826                // coverage (the guard may fail at runtime).
827                let mut _guarded_variants = Vec::new();
828                let mut _guarded_catchall = false;
829                self.bind_match_pattern(
830                    inner,
831                    subject_type,
832                    &mut _guarded_variants,
833                    &mut _guarded_catchall,
834                    line,
835                );
836                let guard_ty = self.infer_expr(condition);
837                self.check_compat(&Type::Bool, &guard_ty, line);
838            }
839            Pattern::Or { patterns, .. } => {
840                for p in patterns {
841                    self.bind_match_pattern(p, subject_type, covered_variants, has_catchall, line);
842                }
843            }
844            Pattern::ListDestructure { elements, rest, .. } => {
845                let elem_type = match subject_type {
846                    Type::List(inner) => *inner.clone(),
847                    Type::Any => Type::Any,
848                    other => {
849                        self.errors.push(TypeError::Mismatch {
850                            expected: "List".into(),
851                            actual: format!("{}", other),
852                            line,
853                        });
854                        Type::Any
855                    }
856                };
857                for p in elements {
858                    self.bind_match_pattern(p, &elem_type, covered_variants, has_catchall, line);
859                }
860                if let Some(rest_name) = rest {
861                    self.locals
862                        .insert(rest_name.clone(), Type::List(Box::new(elem_type)));
863                }
864            }
865            Pattern::TupleDestructure { elements, .. } => match subject_type {
866                Type::Tuple(types) => {
867                    for (idx, p) in elements.iter().enumerate() {
868                        let ty = types.get(idx).cloned().unwrap_or(Type::Any);
869                        self.bind_match_pattern(p, &ty, covered_variants, has_catchall, line);
870                    }
871                }
872                Type::Any => {
873                    for p in elements {
874                        self.bind_match_pattern(
875                            p,
876                            &Type::Any,
877                            covered_variants,
878                            has_catchall,
879                            line,
880                        );
881                    }
882                }
883                other => {
884                    self.errors.push(TypeError::Mismatch {
885                        expected: "Tuple".into(),
886                        actual: format!("{}", other),
887                        line,
888                    });
889                }
890            },
891            Pattern::RecordDestructure {
892                type_name,
893                fields,
894                open: _,
895                ..
896            } => {
897                if let Type::Record(actual_name) = subject_type {
898                    if actual_name != type_name {
899                        self.errors.push(TypeError::Mismatch {
900                            expected: type_name.clone(),
901                            actual: actual_name.clone(),
902                            line,
903                        });
904                    }
905                }
906                for (field_name, field_pat) in fields {
907                    let field_ty = if let Some(ti) = self.symbols.types.get(type_name) {
908                        if let crate::compiler::resolve::TypeInfoKind::Record(def) = &ti.kind {
909                            if let Some(field) = def.fields.iter().find(|f| f.name == *field_name) {
910                                resolve_type_expr(&field.ty, self.symbols)
911                            } else {
912                                Type::Any
913                            }
914                        } else {
915                            Type::Any
916                        }
917                    } else {
918                        Type::Any
919                    };
920                    if let Some(p) = field_pat {
921                        self.bind_match_pattern(p, &field_ty, covered_variants, has_catchall, line);
922                    } else {
923                        self.locals.insert(field_name.clone(), field_ty);
924                    }
925                }
926            }
927            Pattern::TypeCheck {
928                name, type_expr, ..
929            } => {
930                let expected = resolve_type_expr(type_expr, self.symbols);
931                self.check_compat(&expected, subject_type, line);
932                self.locals.insert(name.clone(), expected);
933            }
934            Pattern::Literal(_) => {}
935        }
936    }
937
938    fn infer_expr(&mut self, expr: &Expr) -> Type {
939        match expr {
940            Expr::IntLit(_, _) => Type::Int,
941            Expr::FloatLit(_, _) => Type::Float,
942            Expr::StringLit(_, _) => Type::String,
943            Expr::StringInterp(_, _) => Type::String,
944            Expr::BoolLit(_, _) => Type::Bool,
945            Expr::NullLit(_) => Type::Null,
946            Expr::Ident(name, span) => {
947                if let Some(ty) = self.locals.get(name) {
948                    ty.clone()
949                } else if let Some(const_info) = self.symbols.consts.get(name) {
950                    if let Some(ref ty) = const_info.ty {
951                        resolve_type_expr(ty, self.symbols)
952                    } else if let Some(ref val) = const_info.value {
953                        match val {
954                            Expr::IntLit(_, _) => Type::Int,
955                            Expr::FloatLit(_, _) => Type::Float,
956                            Expr::StringLit(_, _) | Expr::StringInterp(_, _) => Type::String,
957                            Expr::BoolLit(_, _) => Type::Bool,
958                            Expr::NullLit(_) => Type::Null,
959                            Expr::ListLit(_, _) => Type::List(Box::new(Type::Any)),
960                            Expr::MapLit(_, _) => {
961                                Type::Map(Box::new(Type::String), Box::new(Type::Any))
962                            }
963                            _ => Type::Any,
964                        }
965                    } else {
966                        Type::Any
967                    }
968                }
969                // cell ref, tool ref, agent constructor ref, addendum decl refs, type/value references, built-in
970                else if self.symbols.cells.contains_key(name)
971                    || self.symbols.tools.contains_key(name)
972                    || self.symbols.agents.contains_key(name)
973                    || self
974                        .symbols
975                        .addons
976                        .iter()
977                        .any(|a| a.name.as_deref() == Some(name.as_str()))
978                    || self.symbols.types.contains_key(name)
979                    || self.symbols.type_aliases.contains_key(name)
980                    || is_builtin_function(name)
981                {
982                    Type::Any
983                }
984                // built-in
985                else if name == "null" {
986                    Type::Null
987                } else if self.allow_placeholders && is_doc_placeholder_var(name) {
988                    Type::Any
989                } else {
990                    // Check for Enum Variant
991                    let mut found_enum = None;
992                    for (type_name, type_info) in &self.symbols.types {
993                        if let crate::compiler::resolve::TypeInfoKind::Enum(def) = &type_info.kind {
994                            if def.variants.iter().any(|v| v.name == *name) {
995                                found_enum = Some(Type::Enum(type_name.clone()));
996                                break;
997                            }
998                        }
999                    }
1000                    if let Some(ty) = found_enum {
1001                        ty
1002                    } else {
1003                        self.errors.push(TypeError::UndefinedVar {
1004                            name: name.clone(),
1005                            line: span.line,
1006                        });
1007                        Type::Any
1008                    }
1009                }
1010            }
1011            Expr::ListLit(elems, _) => {
1012                if elems.is_empty() {
1013                    Type::List(Box::new(Type::Any))
1014                } else {
1015                    let first = self.infer_expr(&elems[0]);
1016                    for e in &elems[1..] {
1017                        self.infer_expr(e);
1018                    }
1019                    Type::List(Box::new(first))
1020                }
1021            }
1022            Expr::MapLit(pairs, _) => {
1023                if pairs.is_empty() {
1024                    Type::Map(Box::new(Type::String), Box::new(Type::Any))
1025                } else {
1026                    let kt = self.infer_expr(&pairs[0].0);
1027                    let vt = self.infer_expr(&pairs[0].1);
1028                    for (k, v) in &pairs[1..] {
1029                        self.infer_expr(k);
1030                        self.infer_expr(v);
1031                    }
1032                    Type::Map(Box::new(kt), Box::new(vt))
1033                }
1034            }
1035            Expr::RecordLit(name, fields, span) => {
1036                if let Some(ti) = self.symbols.types.get(name) {
1037                    if let crate::compiler::resolve::TypeInfoKind::Record(def) = &ti.kind {
1038                        // Infer generic type arguments from field values
1039                        let generic_args = if !ti.generic_params.is_empty() {
1040                            infer_generic_args_from_fields(
1041                                &ti.generic_params,
1042                                &def.fields,
1043                                fields,
1044                                self.symbols,
1045                                self,
1046                            )
1047                        } else {
1048                            vec![]
1049                        };
1050
1051                        // Build substitution map for generic parameters
1052                        let subst = build_subst(&ti.generic_params, &generic_args);
1053
1054                        // 1. Check provided fields (unknown & type mismatch)
1055                        for (fname, fval) in fields {
1056                            let val_type = self.infer_expr(fval);
1057                            if let Some(field_def) = def.fields.iter().find(|f| f.name == *fname) {
1058                                let expected = resolve_type_expr_with_subst(
1059                                    &field_def.ty,
1060                                    self.symbols,
1061                                    &subst,
1062                                );
1063                                self.check_compat(&expected, &val_type, span.line);
1064                            } else {
1065                                let field_names: Vec<&str> =
1066                                    def.fields.iter().map(|f| f.name.as_str()).collect();
1067                                let suggestions = suggest_similar(fname, &field_names, 2);
1068                                self.errors.push(TypeError::UnknownField {
1069                                    field: fname.clone(),
1070                                    ty: name.clone(),
1071                                    line: span.line,
1072                                    suggestions,
1073                                });
1074                            }
1075                        }
1076                        // 2. Check for missing fields (fields with defaults are optional)
1077                        for field_def in &def.fields {
1078                            if field_def.default_value.is_none()
1079                                && !fields.iter().any(|(fname, _)| fname == &field_def.name)
1080                            {
1081                                self.errors.push(TypeError::Mismatch {
1082                                    expected: format!("field '{}'", field_def.name),
1083                                    actual: "missing".into(),
1084                                    line: span.line,
1085                                });
1086                            }
1087                        }
1088
1089                        // Return the instantiated generic type if applicable
1090                        if !generic_args.is_empty() {
1091                            Type::TypeRef(name.clone(), generic_args)
1092                        } else {
1093                            Type::Record(name.clone())
1094                        }
1095                    } else {
1096                        Type::Record(name.clone())
1097                    }
1098                } else {
1099                    self.errors.push(TypeError::UndefinedType {
1100                        name: name.clone(),
1101                        line: span.line,
1102                    });
1103                    Type::Record(name.clone())
1104                }
1105            }
1106            Expr::BinOp(lhs, op, rhs, _span) => {
1107                let lt = self.infer_expr(lhs);
1108                let rt = self.infer_expr(rhs);
1109                match op {
1110                    BinOp::Add
1111                    | BinOp::Sub
1112                    | BinOp::Mul
1113                    | BinOp::Div
1114                    | BinOp::FloorDiv
1115                    | BinOp::Mod => {
1116                        if lt == Type::Any || rt == Type::Any {
1117                            Type::Any
1118                        } else if (lt == Type::String || rt == Type::String) && *op == BinOp::Add {
1119                            Type::String
1120                        } else if lt == Type::Float || rt == Type::Float {
1121                            Type::Float
1122                        } else {
1123                            Type::Int
1124                        }
1125                    }
1126                    BinOp::Eq
1127                    | BinOp::NotEq
1128                    | BinOp::Lt
1129                    | BinOp::LtEq
1130                    | BinOp::Gt
1131                    | BinOp::GtEq => Type::Bool,
1132                    BinOp::And | BinOp::Or => Type::Bool,
1133                    BinOp::Pow => {
1134                        if lt == Type::Float || rt == Type::Float {
1135                            Type::Float
1136                        } else {
1137                            Type::Int
1138                        }
1139                    }
1140                    BinOp::PipeForward => rt,
1141                    BinOp::Concat => lt,
1142                    BinOp::In => Type::Bool,
1143                    BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => Type::Int,
1144                    BinOp::Shl | BinOp::Shr => {
1145                        if lt != Type::Any && lt != Type::Int {
1146                            self.errors.push(TypeError::Mismatch {
1147                                expected: "Int".into(),
1148                                actual: format!("{}", lt),
1149                                line: _span.line,
1150                            });
1151                        }
1152                        if rt != Type::Any && rt != Type::Int {
1153                            self.errors.push(TypeError::Mismatch {
1154                                expected: "Int".into(),
1155                                actual: format!("{}", rt),
1156                                line: _span.line,
1157                            });
1158                        }
1159                        Type::Int
1160                    }
1161                }
1162            }
1163            Expr::Pipe { left, right, span } => {
1164                let call_expr = desugar_pipe_application(left, right, *span);
1165                self.infer_expr(&call_expr)
1166            }
1167            Expr::Illuminate {
1168                input,
1169                transform,
1170                span,
1171            } => {
1172                let call_expr = desugar_pipe_application(input, transform, *span);
1173                self.infer_expr(&call_expr)
1174            }
1175            Expr::UnaryOp(op, inner, _) => {
1176                let t = self.infer_expr(inner);
1177                match op {
1178                    UnaryOp::Neg => t,
1179                    UnaryOp::Not => Type::Bool,
1180                    UnaryOp::BitNot => Type::Int,
1181                }
1182            }
1183            Expr::Call(callee, args, span) => {
1184                let mut checked_args = Vec::new();
1185                for arg in args {
1186                    match arg {
1187                        CallArg::Positional(e) => {
1188                            let ty = self.infer_expr(e);
1189                            checked_args.push(CheckedCallArg::Positional(ty, e.span().line));
1190                        }
1191                        CallArg::Named(name, e, _) => {
1192                            let ty = self.infer_expr(e);
1193                            checked_args.push(CheckedCallArg::Named(
1194                                name.clone(),
1195                                ty,
1196                                e.span().line,
1197                            ));
1198                        }
1199                        CallArg::Role(_, _, _) => {}
1200                    }
1201                }
1202                // Try to resolve the return type
1203                if let Expr::Ident(name, _) = callee.as_ref() {
1204                    // Check if it's a cell/function call
1205                    if let Some(ci) = self.symbols.cells.get(name) {
1206                        self.check_call_against_signature(&ci.params, &checked_args, span.line);
1207                        if let Some(ref rt) = ci.return_type {
1208                            return resolve_type_expr(rt, self.symbols);
1209                        }
1210                    }
1211                    // Check if it's a record construction
1212                    else if let Some(ti) = self.symbols.types.get(name) {
1213                        if let crate::compiler::resolve::TypeInfoKind::Record(def) = &ti.kind {
1214                            // Infer generic type arguments from constructor arguments
1215                            let generic_args = if !ti.generic_params.is_empty() {
1216                                // Build inference map from named arguments
1217                                let mut inferred: HashMap<String, Type> = HashMap::new();
1218                                for checked_arg in &checked_args {
1219                                    if let CheckedCallArg::Named(fname, arg_ty, _) = checked_arg {
1220                                        if let Some(field_def) =
1221                                            def.fields.iter().find(|f| f.name == *fname)
1222                                        {
1223                                            unify_for_inference(
1224                                                &field_def.ty,
1225                                                arg_ty,
1226                                                self.symbols,
1227                                                &mut inferred,
1228                                            );
1229                                        }
1230                                    }
1231                                }
1232
1233                                ti.generic_params
1234                                    .iter()
1235                                    .map(|p| inferred.get(p).cloned().unwrap_or(Type::Any))
1236                                    .collect()
1237                            } else {
1238                                vec![]
1239                            };
1240
1241                            // Build substitution map for generic parameters
1242                            let subst = build_subst(&ti.generic_params, &generic_args);
1243
1244                            // Check constructor arguments match record fields
1245                            for checked_arg in &checked_args {
1246                                if let CheckedCallArg::Named(fname, arg_ty, line) = checked_arg {
1247                                    if let Some(field_def) =
1248                                        def.fields.iter().find(|f| f.name == *fname)
1249                                    {
1250                                        let expected = resolve_type_expr_with_subst(
1251                                            &field_def.ty,
1252                                            self.symbols,
1253                                            &subst,
1254                                        );
1255                                        self.check_compat(&expected, arg_ty, *line);
1256                                    } else {
1257                                        let field_names: Vec<&str> =
1258                                            def.fields.iter().map(|f| f.name.as_str()).collect();
1259                                        let suggestions = suggest_similar(fname, &field_names, 2);
1260                                        self.errors.push(TypeError::UnknownField {
1261                                            field: fname.clone(),
1262                                            ty: name.clone(),
1263                                            line: *line,
1264                                            suggestions,
1265                                        });
1266                                    }
1267                                }
1268                            }
1269
1270                            // Return the instantiated generic type if applicable
1271                            return if !generic_args.is_empty() {
1272                                Type::TypeRef(name.clone(), generic_args)
1273                            } else {
1274                                Type::Record(name.clone())
1275                            };
1276                        }
1277                    }
1278                }
1279                Type::Any
1280            }
1281            Expr::ToolCall(_, args, _) => {
1282                for arg in args {
1283                    match arg {
1284                        CallArg::Positional(e) | CallArg::Named(_, e, _) => {
1285                            self.infer_expr(e);
1286                        }
1287                        _ => {}
1288                    }
1289                }
1290                Type::Any
1291            }
1292            Expr::DotAccess(obj, field, _span) => {
1293                let ot = self.infer_expr(obj);
1294                match &ot {
1295                    Type::Record(ref name) => {
1296                        if let Some(ti) = self.symbols.types.get(name) {
1297                            if let crate::compiler::resolve::TypeInfoKind::Record(ref rd) = ti.kind
1298                            {
1299                                if let Some(f) = rd.fields.iter().find(|f| f.name == *field) {
1300                                    return resolve_type_expr(&f.ty, self.symbols);
1301                                }
1302                            }
1303                        }
1304                    }
1305                    Type::TypeRef(ref name, ref args) => {
1306                        // Generic type instantiation - apply substitution
1307                        if let Some(ti) = self.symbols.types.get(name) {
1308                            let subst = build_subst(&ti.generic_params, args);
1309                            if let crate::compiler::resolve::TypeInfoKind::Record(ref rd) = ti.kind
1310                            {
1311                                if let Some(f) = rd.fields.iter().find(|f| f.name == *field) {
1312                                    return resolve_type_expr_with_subst(
1313                                        &f.ty,
1314                                        self.symbols,
1315                                        &subst,
1316                                    );
1317                                }
1318                            }
1319                        }
1320                    }
1321                    _ => {}
1322                }
1323                Type::Any
1324            }
1325            Expr::IndexAccess(obj, idx, _) => {
1326                let ot = self.infer_expr(obj);
1327                self.infer_expr(idx);
1328                match ot {
1329                    Type::List(inner) => *inner,
1330                    Type::Map(_, v) => *v,
1331                    _ => Type::Any,
1332                }
1333            }
1334            Expr::RoleBlock(_, content, _) => {
1335                self.infer_expr(content);
1336                Type::String
1337            }
1338            Expr::ExpectSchema(inner, schema_name, _) => {
1339                self.infer_expr(inner);
1340                if self.symbols.types.contains_key(schema_name) {
1341                    Type::Record(schema_name.clone())
1342                } else {
1343                    Type::Any
1344                }
1345            }
1346            Expr::RawStringLit(_, _) => Type::String,
1347            Expr::BytesLit(_, _) => Type::Bytes,
1348            Expr::Lambda {
1349                params,
1350                return_type,
1351                body,
1352                ..
1353            } => {
1354                let saved_locals = self.locals.clone();
1355                let saved_mutables = self.mutables.clone();
1356                let mut param_types = Vec::new();
1357                for p in params {
1358                    let pt = resolve_type_expr(&p.ty, self.symbols);
1359                    if let Some(ref def) = p.default_value {
1360                        self.infer_expr(def);
1361                    }
1362                    self.locals.insert(p.name.clone(), pt.clone());
1363                    self.mutables.insert(p.name.clone(), true);
1364                    param_types.push(pt);
1365                }
1366                let ret = if let Some(ref rt) = return_type {
1367                    resolve_type_expr(rt, self.symbols)
1368                } else {
1369                    match body {
1370                        LambdaBody::Expr(e) => self.infer_expr(e),
1371                        LambdaBody::Block(stmts) => {
1372                            for s in stmts {
1373                                self.check_stmt(s, None);
1374                            }
1375                            Type::Any
1376                        }
1377                    }
1378                };
1379                self.locals = saved_locals;
1380                self.mutables = saved_mutables;
1381                Type::Fn(param_types, Box::new(ret))
1382            }
1383            Expr::TupleLit(elems, _) => {
1384                let types: Vec<_> = elems.iter().map(|e| self.infer_expr(e)).collect();
1385                Type::Tuple(types)
1386            }
1387            Expr::SetLit(elems, _) => {
1388                if elems.is_empty() {
1389                    Type::Set(Box::new(Type::Any))
1390                } else {
1391                    let first = self.infer_expr(&elems[0]);
1392                    for e in &elems[1..] {
1393                        self.infer_expr(e);
1394                    }
1395                    Type::Set(Box::new(first))
1396                }
1397            }
1398            Expr::RangeExpr {
1399                start, end, step, ..
1400            } => {
1401                if let Some(ref s) = start {
1402                    self.infer_expr(s);
1403                }
1404                if let Some(ref e) = end {
1405                    self.infer_expr(e);
1406                }
1407                if let Some(ref st) = step {
1408                    self.infer_expr(st);
1409                }
1410                Type::List(Box::new(Type::Int))
1411            }
1412            Expr::TryExpr(inner, _) => {
1413                let t = self.infer_expr(inner);
1414                // If inner is Result[Ok, Err], return Ok type (propagating Err)
1415                if let Type::Result(ok, _) = t {
1416                    *ok
1417                } else {
1418                    t
1419                }
1420            }
1421            Expr::NullCoalesce(lhs, rhs, _) => {
1422                let lt = self.infer_expr(lhs);
1423                let rt = self.infer_expr(rhs);
1424                // If lhs is T | Null, result is T (or rhs type)
1425                match lt {
1426                    Type::Union(ref types) => {
1427                        let non_null: Vec<_> = types
1428                            .iter()
1429                            .filter(|t| **t != Type::Null)
1430                            .cloned()
1431                            .collect();
1432                        if non_null.len() == 1 {
1433                            non_null.into_iter().next().unwrap()
1434                        } else if non_null.is_empty() {
1435                            rt
1436                        } else {
1437                            Type::Union(non_null)
1438                        }
1439                    }
1440                    Type::Null => rt,
1441                    _ => lt,
1442                }
1443            }
1444            Expr::NullSafeAccess(obj, field, _span) => {
1445                let ot = self.infer_expr(obj);
1446                // Result is T | Null
1447                let field_type = if let Type::Record(ref name) = ot {
1448                    if let Some(ti) = self.symbols.types.get(name) {
1449                        if let crate::compiler::resolve::TypeInfoKind::Record(ref rd) = ti.kind {
1450                            if let Some(f) = rd.fields.iter().find(|f| f.name == *field) {
1451                                resolve_type_expr(&f.ty, self.symbols)
1452                            } else {
1453                                Type::Any
1454                            }
1455                        } else {
1456                            Type::Any
1457                        }
1458                    } else {
1459                        Type::Any
1460                    }
1461                } else {
1462                    Type::Any
1463                };
1464                Type::Union(vec![field_type, Type::Null])
1465            }
1466            Expr::NullSafeIndex(obj, idx, _) => {
1467                let ot = self.infer_expr(obj);
1468                self.infer_expr(idx);
1469                let elem_type = match ot {
1470                    Type::List(inner) => *inner,
1471                    _ => Type::Any,
1472                };
1473                Type::Union(vec![elem_type, Type::Null])
1474            }
1475            Expr::NullAssert(inner, _) => {
1476                let t = self.infer_expr(inner);
1477                // Strip Null from union types
1478                match t {
1479                    Type::Union(ref types) => {
1480                        let non_null: Vec<_> = types
1481                            .iter()
1482                            .filter(|t| **t != Type::Null)
1483                            .cloned()
1484                            .collect();
1485                        if non_null.len() == 1 {
1486                            non_null.into_iter().next().unwrap()
1487                        } else if non_null.is_empty() {
1488                            Type::Any
1489                        } else {
1490                            Type::Union(non_null)
1491                        }
1492                    }
1493                    _ => t,
1494                }
1495            }
1496            Expr::SpreadExpr(inner, _) => self.infer_expr(inner),
1497            Expr::IsType {
1498                expr: inner,
1499                type_name,
1500                span,
1501            } => {
1502                self.infer_expr(inner);
1503                // Validate that the target type exists
1504                let is_known_type = matches!(
1505                    type_name.as_str(),
1506                    "Int" | "Float" | "String" | "Bool" | "Bytes" | "Json" | "Null"
1507                ) || self.symbols.types.contains_key(type_name)
1508                    || self.symbols.type_aliases.contains_key(type_name);
1509                if !is_known_type && !self.allow_placeholders {
1510                    self.errors.push(TypeError::UndefinedType {
1511                        name: type_name.clone(),
1512                        line: span.line,
1513                    });
1514                }
1515                Type::Bool
1516            }
1517            Expr::TypeCast {
1518                expr: inner,
1519                target_type,
1520                span,
1521            } => {
1522                self.infer_expr(inner);
1523                match target_type.as_str() {
1524                    "Int" => Type::Int,
1525                    "Float" => Type::Float,
1526                    "String" => Type::String,
1527                    "Bool" => Type::Bool,
1528                    "Bytes" => Type::Bytes,
1529                    "Json" => Type::Json,
1530                    _ => {
1531                        if let Some(ti) = self.symbols.types.get(target_type) {
1532                            use crate::compiler::resolve::TypeInfoKind;
1533                            match &ti.kind {
1534                                TypeInfoKind::Record(_) => Type::Record(target_type.clone()),
1535                                TypeInfoKind::Enum(_) => Type::Enum(target_type.clone()),
1536                                TypeInfoKind::Builtin => Type::Record(target_type.clone()),
1537                            }
1538                        } else if self.allow_placeholders {
1539                            Type::Any
1540                        } else {
1541                            self.errors.push(TypeError::UndefinedType {
1542                                name: target_type.clone(),
1543                                line: span.line,
1544                            });
1545                            Type::Any
1546                        }
1547                    }
1548                }
1549            }
1550            Expr::IfExpr {
1551                cond,
1552                then_val,
1553                else_val,
1554                ..
1555            } => {
1556                let ct = self.infer_expr(cond);
1557                self.check_compat(&Type::Bool, &ct, cond.span().line);
1558                let tt = self.infer_expr(then_val);
1559                self.infer_expr(else_val);
1560                tt
1561            }
1562            Expr::AwaitExpr(inner, _) => self.infer_expr(inner),
1563            Expr::Comprehension {
1564                body,
1565                var,
1566                iter,
1567                condition,
1568                kind,
1569                span: _,
1570            } => {
1571                let iter_type = self.infer_expr(iter);
1572                let elem_type = match &iter_type {
1573                    Type::List(inner) => *inner.clone(),
1574                    Type::Set(inner) => *inner.clone(),
1575                    _ => Type::Any,
1576                };
1577                self.locals.insert(var.clone(), elem_type);
1578                if let Some(ref cond) = condition {
1579                    let ct = self.infer_expr(cond);
1580                    self.check_compat(&Type::Bool, &ct, cond.span().line);
1581                }
1582                let body_type = self.infer_expr(body);
1583                match kind {
1584                    ComprehensionKind::List => Type::List(Box::new(body_type)),
1585                    ComprehensionKind::Set => Type::Set(Box::new(body_type)),
1586                    ComprehensionKind::Map => Type::Any, // map comprehension needs key+value
1587                }
1588            }
1589            Expr::MatchExpr {
1590                subject,
1591                arms,
1592                span,
1593            } => {
1594                let subject_type = self.infer_expr(subject);
1595                let mut covered_variants = Vec::new();
1596                let mut has_catchall = false;
1597                let mut result_type = Type::Any;
1598
1599                for arm in arms {
1600                    self.bind_match_pattern(
1601                        &arm.pattern,
1602                        &subject_type,
1603                        &mut covered_variants,
1604                        &mut has_catchall,
1605                        arm.span.line,
1606                    );
1607                    for s in &arm.body {
1608                        self.check_stmt(s, None);
1609                    }
1610                    // Infer type from last expression in arm body
1611                    if let Some(Stmt::Expr(es)) = arm.body.last() {
1612                        result_type = self.infer_expr(&es.expr);
1613                    } else if let Some(Stmt::Return(rs)) = arm.body.last() {
1614                        result_type = self.infer_expr(&rs.value);
1615                    }
1616                }
1617
1618                // Exhaustiveness check for enums
1619                if let Type::Enum(ref name) = subject_type {
1620                    if !has_catchall {
1621                        if let Some(ti) = self.symbols.types.get(name) {
1622                            if let crate::compiler::resolve::TypeInfoKind::Enum(def) = &ti.kind {
1623                                let missing: Vec<_> = def
1624                                    .variants
1625                                    .iter()
1626                                    .filter(|v| !covered_variants.contains(&v.name))
1627                                    .map(|v| v.name.clone())
1628                                    .collect();
1629                                if !missing.is_empty() {
1630                                    self.errors.push(TypeError::IncompleteMatch {
1631                                        enum_name: name.clone(),
1632                                        missing,
1633                                        line: span.line,
1634                                    });
1635                                }
1636                            }
1637                        }
1638                    }
1639                }
1640
1641                result_type
1642            }
1643            Expr::BlockExpr(stmts, _) => {
1644                for s in stmts {
1645                    self.check_stmt(s, None);
1646                }
1647                // Infer type from last expression in block
1648                if let Some(Stmt::Expr(es)) = stmts.last() {
1649                    self.infer_expr(&es.expr)
1650                } else {
1651                    Type::Any
1652                }
1653            }
1654        }
1655    }
1656
1657    fn check_compat(&mut self, expected: &Type, actual: &Type, line: usize) {
1658        if *expected == Type::Any || *actual == Type::Any {
1659            return;
1660        }
1661        if type_contains_any(expected) || type_contains_any(actual) {
1662            return;
1663        }
1664        if expected == actual {
1665            return;
1666        }
1667
1668        // Union compatibility: actual is compatible if it matches any member of expected union
1669        if let Type::Union(ref types) = expected {
1670            if types.iter().any(|t| t == actual || *t == Type::Any) {
1671                return;
1672            }
1673        }
1674        // actual is union: compatible if all members are compatible with expected
1675        if let Type::Union(ref types) = actual {
1676            if types.iter().any(|t| t == expected || *t == Type::Any) {
1677                return;
1678            }
1679        }
1680
1681        // Null is compatible with T | Null unions
1682        if *actual == Type::Null {
1683            if let Type::Union(ref types) = expected {
1684                if types.contains(&Type::Null) {
1685                    return;
1686                }
1687            }
1688        }
1689
1690        if *expected == Type::Float && *actual == Type::Int {
1691            return;
1692        }
1693
1694        // Result compatibility: Result[A, B] is compatible with Result[C, D] if A compat C, B compat D
1695        // Allow implicit wrapping into `ok(...)` when a plain value is returned for a Result type.
1696        if let Type::Result(ok, _) = expected {
1697            if **ok == *actual || **ok == Type::Any || *actual == Type::Any {
1698                return;
1699            }
1700        }
1701        // Generic type refs are compatible if the base name matches and args are compatible
1702        if let (Type::TypeRef(n1, args1), Type::TypeRef(n2, args2)) = (expected, actual) {
1703            if n1 == n2 {
1704                // Check that all type arguments are compatible
1705                if args1.len() == args2.len() {
1706                    let all_compat = args1
1707                        .iter()
1708                        .zip(args2.iter())
1709                        .all(|(a1, a2)| a1 == a2 || *a1 == Type::Any || *a2 == Type::Any);
1710                    if all_compat {
1711                        return;
1712                    }
1713                } else {
1714                    return; // Arity mismatch, but already reported in resolve
1715                }
1716            }
1717        }
1718
1719        // Allow TypeRef to be compatible with its base Record type
1720        if let (Type::Record(name1), Type::TypeRef(name2, _)) = (expected, actual) {
1721            if name1 == name2 {
1722                return;
1723            }
1724        }
1725        if let (Type::TypeRef(name1, _), Type::Record(name2)) = (expected, actual) {
1726            if name1 == name2 {
1727                return;
1728            }
1729        }
1730
1731        self.errors.push(TypeError::Mismatch {
1732            expected: format!("{}", expected),
1733            actual: format!("{}", actual),
1734            line,
1735        });
1736    }
1737}
1738
1739fn parse_directive_bool(program: &Program, name: &str) -> Option<bool> {
1740    if let Some(directive) = program
1741        .directives
1742        .iter()
1743        .find(|d| d.name.eq_ignore_ascii_case(name))
1744    {
1745        let raw = directive
1746            .value
1747            .as_deref()
1748            .unwrap_or("true")
1749            .trim()
1750            .to_ascii_lowercase();
1751        return match raw.as_str() {
1752            "1" | "true" | "yes" | "on" => Some(true),
1753            "0" | "false" | "no" | "off" => Some(false),
1754            _ => None,
1755        };
1756    }
1757
1758    // Support attribute-style toggles, e.g. `@doc_mode true` parsed as Addon
1759    let has_attr = program.items.iter().any(|item| {
1760        matches!(
1761            item,
1762            Item::Addon(AddonDecl {
1763                kind,
1764                name: Some(attr_name),
1765                ..
1766            }) if kind == "attribute" && attr_name.eq_ignore_ascii_case(name)
1767        )
1768    });
1769    if has_attr {
1770        Some(true)
1771    } else {
1772        None
1773    }
1774}
1775
1776/// Typecheck a program.
1777pub fn typecheck(program: &Program, symbols: &SymbolTable) -> Result<(), Vec<TypeError>> {
1778    let strict = parse_directive_bool(program, "strict").unwrap_or(true);
1779    let doc_mode = parse_directive_bool(program, "doc_mode").unwrap_or(false);
1780    let allow_placeholders = doc_mode || !strict;
1781    let mut checker = TypeChecker::new(symbols, allow_placeholders);
1782    for item in &program.items {
1783        match item {
1784            Item::Cell(c) => checker.check_cell(c),
1785            Item::Agent(a) => {
1786                for cell in &a.cells {
1787                    checker.check_agent_cell(cell);
1788                }
1789            }
1790            Item::Process(p) => {
1791                for cell in &p.cells {
1792                    checker.check_cell(cell);
1793                }
1794            }
1795            Item::Effect(e) => {
1796                for op in &e.operations {
1797                    checker.check_cell(op);
1798                }
1799            }
1800            Item::Handler(h) => {
1801                for handle in &h.handles {
1802                    checker.check_cell(handle);
1803                }
1804            }
1805            _ => {}
1806        }
1807    }
1808    if checker.errors.is_empty() {
1809        Ok(())
1810    } else {
1811        Err(checker.errors)
1812    }
1813}
1814
1815#[cfg(test)]
1816mod tests {
1817    use super::*;
1818    use crate::compiler::lexer::Lexer;
1819    use crate::compiler::parser::Parser;
1820    use crate::compiler::resolve;
1821
1822    fn typecheck_src(src: &str) -> Result<(), Vec<TypeError>> {
1823        let mut lexer = Lexer::new(src, 1, 0);
1824        let tokens = lexer.tokenize().unwrap();
1825        let mut parser = Parser::new(tokens);
1826        let prog = parser.parse_program(vec![]).unwrap();
1827        let symbols = resolve::resolve(&prog).unwrap();
1828        typecheck(&prog, &symbols)
1829    }
1830
1831    #[test]
1832    fn test_typecheck_basic() {
1833        typecheck_src("cell add(a: Int, b: Int) -> Int\n  return a + b\nend").unwrap();
1834    }
1835
1836    #[test]
1837    fn test_typecheck_undefined_var() {
1838        let err = typecheck_src("cell bad() -> Int\n  return missing_var\nend").unwrap_err();
1839        assert!(!err.is_empty());
1840    }
1841
1842    #[test]
1843    fn test_type_alias_resolves_in_typecheck() {
1844        // Type alias should resolve to the underlying type
1845        typecheck_src("type UserId = String\n\ncell greet(id: UserId) -> String\n  return id\nend")
1846            .unwrap();
1847    }
1848
1849    #[test]
1850    fn test_doc_mode_allows_any_undefined_var() {
1851        // In doc_mode, any undefined variable should be allowed
1852        typecheck_src(
1853            "@doc_mode true\n\ncell example() -> Int\n  return completely_unknown_var_xyz\nend",
1854        )
1855        .unwrap();
1856    }
1857
1858    #[test]
1859    fn test_strict_mode_catches_undefined_var() {
1860        // In strict mode (default), undefined variables should be caught
1861        let err = typecheck_src("cell example() -> Int\n  return completely_unknown_var_xyz\nend")
1862            .unwrap_err();
1863        assert!(err.iter().any(|e| matches!(e, TypeError::UndefinedVar { name, .. } if name == "completely_unknown_var_xyz")));
1864    }
1865
1866    #[test]
1867    fn test_is_doc_placeholder_var_rejects_dunder_names() {
1868        // Names starting with __ should not be placeholders (internal/generated names)
1869        assert!(!is_doc_placeholder_var("__pattern"));
1870        assert!(!is_doc_placeholder_var("__tuple"));
1871        // Normal names are fine
1872        assert!(is_doc_placeholder_var("x"));
1873        assert!(is_doc_placeholder_var("my_variable"));
1874    }
1875
1876    #[test]
1877    fn test_type_alias_basic() {
1878        // Basic type alias to primitive type
1879        typecheck_src("type UserId = String\n\ncell greet(id: UserId) -> UserId\n  return id\nend")
1880            .unwrap();
1881    }
1882
1883    #[test]
1884    fn test_type_alias_complex() {
1885        // Type alias to complex type
1886        typecheck_src(
1887            "type StringList = list[String]\n\ncell make_list() -> StringList\n  return [\"a\", \"b\"]\nend",
1888        )
1889        .unwrap();
1890    }
1891
1892    #[test]
1893    fn test_type_alias_in_record() {
1894        // Type alias used in record field
1895        typecheck_src(
1896            "type Email = String\n\nrecord User\n  email: Email\nend\n\ncell get_email(u: User) -> Email\n  return u.email\nend",
1897        )
1898        .unwrap();
1899    }
1900
1901    #[test]
1902    fn test_type_alias_chained() {
1903        // Chained type aliases: A -> B -> String
1904        typecheck_src(
1905            "type UserId = String\ntype Id = UserId\n\ncell make_id() -> Id\n  return \"123\"\nend",
1906        )
1907        .unwrap();
1908    }
1909
1910    #[test]
1911    fn test_is_type_returns_bool() {
1912        // IsType expression should return Bool
1913        typecheck_src("cell check(x: Int) -> Bool\n  return x is Int\nend").unwrap();
1914    }
1915
1916    #[test]
1917    fn test_type_cast_returns_target_type() {
1918        // TypeCast expression should return the target type
1919        typecheck_src("cell convert(x: Float) -> Int\n  return x as Int\nend").unwrap();
1920    }
1921
1922    #[test]
1923    fn test_compound_assign_bitwise_requires_int() {
1924        // Bitwise compound assignment on non-Int should error
1925        let err =
1926            typecheck_src("cell bad() -> String\n  let x = \"hello\"\n  x &= 1\n  return x\nend")
1927                .unwrap_err();
1928        assert!(err
1929            .iter()
1930            .any(|e| matches!(e, TypeError::Mismatch { expected, .. } if expected == "Int")));
1931    }
1932
1933    #[test]
1934    fn test_compound_assign_add_is_valid() {
1935        // Basic compound assignment should work
1936        typecheck_src("cell inc() -> Int\n  let x = 1\n  x += 2\n  return x\nend").unwrap();
1937    }
1938
1939    #[test]
1940    fn test_shift_operators_return_int() {
1941        // Shift operators should return Int
1942        typecheck_src("cell shift(a: Int, b: Int) -> Int\n  return a << b\nend").unwrap();
1943    }
1944
1945    #[test]
1946    fn test_shift_operators_require_int_operands() {
1947        // Shift with non-Int operand should error
1948        let err =
1949            typecheck_src("cell bad(a: String, b: Int) -> Int\n  return a << b\nend").unwrap_err();
1950        assert!(err
1951            .iter()
1952            .any(|e| matches!(e, TypeError::Mismatch { expected, .. } if expected == "Int")));
1953    }
1954
1955    #[test]
1956    fn test_validation_error_not_hardcoded() {
1957        // ValidationError is no longer hardcoded as a builtin type;
1958        // it resolves via the normal symbol table lookup
1959        let err =
1960            typecheck_src("cell test() -> Int\n  let x: ValidationError = null\n  return 1\nend");
1961        // Should either succeed (if resolved as Any) or fail gracefully
1962        // The key assertion: it doesn't crash and doesn't produce a Record("ValidationError") type
1963        // without a definition in the symbol table
1964        let _ = err;
1965    }
1966}