Skip to main content

tl_types/
checker.rs

1// ThinkingLanguage — Type Checker
2// Licensed under MIT OR Apache-2.0
3//
4// Walks the AST, builds type environment, infers types from expressions,
5// and checks annotations. Gradual: unannotated code = `any`, always passes.
6
7use std::collections::HashSet;
8use tl_ast::{Expr, MatchArm, Pattern, Program, Stmt, StmtKind};
9use tl_errors::Span;
10
11use crate::convert::{convert_type_expr, convert_type_expr_with_params};
12use crate::infer::infer_expr;
13use crate::{FnSig, TraitInfo, Type, TypeEnv, is_compatible};
14
15/// A type error with source location.
16#[derive(Debug, Clone)]
17pub struct TypeError {
18    pub message: String,
19    pub span: Span,
20    pub expected: Option<String>,
21    pub found: Option<String>,
22    pub hint: Option<String>,
23}
24
25impl std::fmt::Display for TypeError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}", self.message)?;
28        if let (Some(expected), Some(found)) = (&self.expected, &self.found) {
29            write!(f, " (expected `{expected}`, found `{found}`)")?;
30        }
31        Ok(())
32    }
33}
34
35/// Configuration for the type checker.
36#[derive(Default)]
37pub struct CheckerConfig {
38    /// If true, require type annotations on function parameters.
39    pub strict: bool,
40}
41
42/// Result of type checking a program.
43pub struct CheckResult {
44    pub errors: Vec<TypeError>,
45    pub warnings: Vec<TypeError>,
46}
47
48impl CheckResult {
49    pub fn has_errors(&self) -> bool {
50        !self.errors.is_empty()
51    }
52}
53
54/// Type check a program. Returns errors and warnings.
55pub fn check_program(program: &Program, config: &CheckerConfig) -> CheckResult {
56    let mut checker = TypeChecker {
57        env: TypeEnv::new(),
58        errors: Vec::new(),
59        warnings: Vec::new(),
60        config,
61        current_fn_return: None,
62        defined_vars: Vec::new(),
63        used_vars: HashSet::new(),
64        imported_names: Vec::new(),
65        used_imports: HashSet::new(),
66        in_async_fn: false,
67        consumed_vars: std::collections::HashMap::new(),
68    };
69
70    // First pass: register all top-level functions and types
71    for stmt in &program.statements {
72        checker.register_top_level(stmt);
73    }
74
75    // Second pass: check all statements
76    checker.check_body(&program.statements);
77
78    // Check for unused variables at top-level scope
79    checker.check_unused_vars();
80
81    // Check for unused imports
82    checker.check_unused_imports();
83
84    CheckResult {
85        errors: checker.errors,
86        warnings: checker.warnings,
87    }
88}
89
90struct TypeChecker<'a> {
91    env: TypeEnv,
92    errors: Vec<TypeError>,
93    warnings: Vec<TypeError>,
94    config: &'a CheckerConfig,
95    /// The return type of the current function being checked (None if top-level).
96    current_fn_return: Option<Type>,
97    /// Variables defined in the current scope: (name, span, scope_depth)
98    defined_vars: Vec<(String, Span, u32)>,
99    /// Variables that have been used/referenced
100    used_vars: HashSet<String>,
101    /// Names imported via `use` statements: (name, span)
102    imported_names: Vec<(String, Span)>,
103    /// Import names that have been referenced
104    used_imports: HashSet<String>,
105    /// Whether the current function is async (for await checking)
106    in_async_fn: bool,
107    /// Variables consumed by pipe-move: name -> span where consumed
108    consumed_vars: std::collections::HashMap<String, Span>,
109}
110
111/// Check if a name follows snake_case convention.
112pub fn is_snake_case(s: &str) -> bool {
113    if s.is_empty() || s.starts_with('_') {
114        return true; // _-prefixed names are always ok
115    }
116    s.chars()
117        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
118}
119
120/// Check if a name follows PascalCase convention.
121pub fn is_pascal_case(s: &str) -> bool {
122    if s.is_empty() {
123        return true;
124    }
125    let first = s.chars().next().unwrap();
126    first.is_ascii_uppercase() && !s.contains('_')
127}
128
129/// Check if a statement is a control flow terminator (return/break/continue/throw).
130fn is_terminator(kind: &StmtKind) -> bool {
131    matches!(
132        kind,
133        StmtKind::Return(_) | StmtKind::Break | StmtKind::Continue | StmtKind::Throw(_)
134    )
135}
136
137impl<'a> TypeChecker<'a> {
138    fn current_scope_depth(&self) -> u32 {
139        self.env.scope_depth()
140    }
141
142    fn register_top_level(&mut self, stmt: &Stmt) {
143        match &stmt.kind {
144            StmtKind::FnDecl {
145                name,
146                type_params,
147                params,
148                return_type,
149                ..
150            } => {
151                let param_types: Vec<(String, Type)> = params
152                    .iter()
153                    .map(|p| {
154                        let ty = p
155                            .type_ann
156                            .as_ref()
157                            .map(|t| convert_type_expr_with_params(t, type_params))
158                            .unwrap_or(Type::Any);
159                        (p.name.clone(), ty)
160                    })
161                    .collect();
162                let ret = return_type
163                    .as_ref()
164                    .map(|t| convert_type_expr_with_params(t, type_params))
165                    .unwrap_or(Type::Any);
166                self.env.define_fn(
167                    name.clone(),
168                    FnSig {
169                        params: param_types,
170                        ret,
171                    },
172                );
173                // Also define the function name as a variable of function type
174                let fn_type = Type::Function {
175                    params: params
176                        .iter()
177                        .map(|p| {
178                            p.type_ann
179                                .as_ref()
180                                .map(|t| convert_type_expr_with_params(t, type_params))
181                                .unwrap_or(Type::Any)
182                        })
183                        .collect(),
184                    ret: Box::new(
185                        return_type
186                            .as_ref()
187                            .map(|t| convert_type_expr_with_params(t, type_params))
188                            .unwrap_or(Type::Any),
189                    ),
190                };
191                self.env.define(name.clone(), fn_type);
192                // Mark function names as "used" — they are declarations, not unused vars
193                self.used_vars.insert(name.clone());
194            }
195            StmtKind::StructDecl { name, fields, .. } => {
196                let field_types: Vec<(String, Type)> = fields
197                    .iter()
198                    .map(|f| (f.name.clone(), convert_type_expr(&f.type_ann)))
199                    .collect();
200                self.env.define_struct(name.clone(), field_types);
201                self.env.define(name.clone(), Type::Struct(name.clone()));
202                self.used_vars.insert(name.clone());
203            }
204            StmtKind::EnumDecl { name, variants, .. } => {
205                let variant_types: Vec<(String, Vec<Type>)> = variants
206                    .iter()
207                    .map(|v| {
208                        (
209                            v.name.clone(),
210                            v.fields.iter().map(convert_type_expr).collect(),
211                        )
212                    })
213                    .collect();
214                self.env.define_enum(name.clone(), variant_types);
215                self.env.define(name.clone(), Type::Enum(name.clone()));
216                self.used_vars.insert(name.clone());
217            }
218            StmtKind::TraitDef { name, methods, .. } => {
219                let method_sigs: Vec<(String, Vec<Type>, Type)> = methods
220                    .iter()
221                    .map(|m| {
222                        let param_types: Vec<Type> = m
223                            .params
224                            .iter()
225                            .map(|p| {
226                                p.type_ann
227                                    .as_ref()
228                                    .map(convert_type_expr)
229                                    .unwrap_or(Type::Any)
230                            })
231                            .collect();
232                        let ret = m
233                            .return_type
234                            .as_ref()
235                            .map(convert_type_expr)
236                            .unwrap_or(Type::Any);
237                        (m.name.clone(), param_types, ret)
238                    })
239                    .collect();
240                self.env.define_trait(
241                    name.clone(),
242                    TraitInfo {
243                        name: name.clone(),
244                        methods: method_sigs,
245                        supertrait: None,
246                    },
247                );
248                self.used_vars.insert(name.clone());
249            }
250            _ => {}
251        }
252    }
253
254    /// Check a body of statements, tracking unreachable code.
255    fn check_body(&mut self, stmts: &[Stmt]) {
256        let mut terminated = false;
257        for stmt in stmts {
258            if terminated {
259                self.warnings.push(TypeError {
260                    message: "Unreachable code".to_string(),
261                    span: stmt.span,
262                    expected: None,
263                    found: None,
264                    hint: Some("This code will never be executed".to_string()),
265                });
266                // Only warn once per block
267                return;
268            }
269            self.check_stmt(stmt);
270            if is_terminator(&stmt.kind) {
271                terminated = true;
272            }
273        }
274    }
275
276    /// Check a body and return whether ALL paths terminate.
277    #[allow(dead_code)]
278    fn check_body_terminates(&mut self, stmts: &[Stmt]) -> bool {
279        let mut terminated = false;
280        for stmt in stmts {
281            if terminated {
282                self.warnings.push(TypeError {
283                    message: "Unreachable code".to_string(),
284                    span: stmt.span,
285                    expected: None,
286                    found: None,
287                    hint: Some("This code will never be executed".to_string()),
288                });
289                return true;
290            }
291            self.check_stmt(stmt);
292            if is_terminator(&stmt.kind) {
293                terminated = true;
294            }
295        }
296        terminated
297    }
298
299    /// Mark a variable name as used (for unused variable tracking).
300    fn mark_used_in_expr(&mut self, expr: &Expr) {
301        match expr {
302            Expr::Ident(name) => {
303                self.used_vars.insert(name.clone());
304                if self.imported_names.iter().any(|(n, _)| n == name) {
305                    self.used_imports.insert(name.clone());
306                }
307                // Check for use-after-move
308                if self.consumed_vars.contains_key(name) {
309                    self.errors.push(TypeError {
310                        message: format!("Use of moved value `{name}`. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy."),
311                        span: Span::new(0, 0),
312                        expected: None,
313                        found: None,
314                        hint: Some(format!("Use `{name}.clone() |> ...` to keep a copy")),
315                    });
316                }
317            }
318            Expr::BinOp { left, right, .. } => {
319                self.mark_used_in_expr(left);
320                self.mark_used_in_expr(right);
321            }
322            Expr::UnaryOp { expr, .. } => self.mark_used_in_expr(expr),
323            Expr::Call { function, args } => {
324                self.mark_used_in_expr(function);
325                for a in args {
326                    self.mark_used_in_expr(a);
327                }
328            }
329            Expr::Member { object, .. } => self.mark_used_in_expr(object),
330            Expr::Index { object, index } => {
331                self.mark_used_in_expr(object);
332                self.mark_used_in_expr(index);
333            }
334            Expr::List(elems) => {
335                for e in elems {
336                    self.mark_used_in_expr(e);
337                }
338            }
339            Expr::Map(entries) => {
340                for (k, v) in entries {
341                    self.mark_used_in_expr(k);
342                    self.mark_used_in_expr(v);
343                }
344            }
345            Expr::Pipe { left, right } => {
346                self.mark_used_in_expr(left);
347                // Mark left-side identifier as consumed by pipe-move
348                if let Expr::Ident(name) = left.as_ref() {
349                    self.consumed_vars.insert(name.clone(), Span::new(0, 0));
350                }
351                self.mark_used_in_expr(right);
352            }
353            Expr::Closure { body, .. } => match body {
354                tl_ast::ClosureBody::Expr(e) => self.mark_used_in_expr(e),
355                tl_ast::ClosureBody::Block { stmts, expr } => {
356                    for s in stmts {
357                        self.mark_used_in_stmt(s);
358                    }
359                    if let Some(e) = expr {
360                        self.mark_used_in_expr(e);
361                    }
362                }
363            },
364            Expr::NullCoalesce { expr, default } => {
365                self.mark_used_in_expr(expr);
366                self.mark_used_in_expr(default);
367            }
368            Expr::Assign { target, value } => {
369                self.mark_used_in_expr(target);
370                self.mark_used_in_expr(value);
371                // Clear move state on reassignment
372                if let Expr::Ident(name) = target.as_ref() {
373                    self.consumed_vars.remove(name);
374                }
375            }
376            Expr::StructInit { name, fields } => {
377                self.used_vars.insert(name.clone());
378                for (_, v) in fields {
379                    self.mark_used_in_expr(v);
380                }
381            }
382            Expr::EnumVariant {
383                enum_name, args, ..
384            } => {
385                self.used_vars.insert(enum_name.clone());
386                for a in args {
387                    self.mark_used_in_expr(a);
388                }
389            }
390            Expr::Range { start, end } => {
391                self.mark_used_in_expr(start);
392                self.mark_used_in_expr(end);
393            }
394            Expr::Block { stmts, expr } => {
395                for s in stmts {
396                    self.mark_used_in_stmt(s);
397                }
398                if let Some(e) = expr {
399                    self.mark_used_in_expr(e);
400                }
401            }
402            Expr::Match { subject, arms } => {
403                self.mark_used_in_expr(subject);
404                for arm in arms {
405                    self.mark_used_in_pattern(&arm.pattern);
406                    if let Some(guard) = &arm.guard {
407                        self.mark_used_in_expr(guard);
408                    }
409                    self.mark_used_in_expr(&arm.body);
410                }
411            }
412            Expr::Case { arms } => {
413                for arm in arms {
414                    self.mark_used_in_pattern(&arm.pattern);
415                    if let Some(guard) = &arm.guard {
416                        self.mark_used_in_expr(guard);
417                    }
418                    self.mark_used_in_expr(&arm.body);
419                }
420            }
421            Expr::Await(inner) => {
422                self.mark_used_in_expr(inner);
423                if !self.in_async_fn {
424                    self.warnings.push(TypeError {
425                        message: "await used outside of async function".to_string(),
426                        span: Span::new(0, 0),
427                        expected: None,
428                        found: None,
429                        hint: Some("Use `async fn` to declare an async function".to_string()),
430                    });
431                }
432            }
433            Expr::Try(inner) => self.mark_used_in_expr(inner),
434            Expr::Yield(Some(inner)) => self.mark_used_in_expr(inner),
435            Expr::NamedArg { value, .. } => self.mark_used_in_expr(value),
436            _ => {}
437        }
438    }
439
440    fn mark_used_in_pattern(&mut self, pattern: &Pattern) {
441        match pattern {
442            Pattern::Literal(expr) => self.mark_used_in_expr(expr),
443            Pattern::Enum { args, .. } => {
444                for arg in args {
445                    self.mark_used_in_pattern(arg);
446                }
447            }
448            Pattern::Struct { fields, .. } => {
449                for f in fields {
450                    if let Some(p) = &f.pattern {
451                        self.mark_used_in_pattern(p);
452                    }
453                }
454            }
455            Pattern::List { elements, .. } => {
456                for e in elements {
457                    self.mark_used_in_pattern(e);
458                }
459            }
460            Pattern::Or(pats) => {
461                for p in pats {
462                    self.mark_used_in_pattern(p);
463                }
464            }
465            Pattern::Wildcard | Pattern::Binding(_) => {}
466        }
467    }
468
469    fn mark_used_in_stmt(&mut self, stmt: &Stmt) {
470        match &stmt.kind {
471            StmtKind::Expr(e) | StmtKind::Throw(e) | StmtKind::Return(Some(e)) => {
472                self.mark_used_in_expr(e);
473            }
474            StmtKind::Let { value, .. } | StmtKind::LetDestructure { value, .. } => {
475                self.mark_used_in_expr(value)
476            }
477            _ => {}
478        }
479    }
480
481    fn check_stmt(&mut self, stmt: &Stmt) {
482        match &stmt.kind {
483            StmtKind::Let {
484                name,
485                type_ann,
486                value,
487                ..
488            } => {
489                // Mark all variables used in the value expression
490                self.mark_used_in_expr(value);
491
492                // Check struct init if the value is a struct init expression
493                if let Expr::StructInit {
494                    name: sname,
495                    fields,
496                } = value
497                {
498                    self.check_struct_init(sname, fields, stmt.span);
499                }
500
501                // Check match exhaustiveness in value expression
502                self.check_match_exhaustiveness_in_expr(value, stmt.span);
503
504                // Check closure return type annotation matches body
505                self.check_closure_return_type(value, stmt.span);
506
507                let inferred = infer_expr(value, &self.env);
508                if let Some(ann) = type_ann {
509                    let expected = convert_type_expr(ann);
510                    if !is_compatible(&expected, &inferred) {
511                        self.errors.push(TypeError {
512                            message: format!("Type mismatch in let binding `{name}`"),
513                            span: stmt.span,
514                            expected: Some(expected.to_string()),
515                            found: Some(inferred.to_string()),
516                            hint: None,
517                        });
518                    }
519                    self.env.define(name.clone(), expected);
520                } else {
521                    self.env.define(name.clone(), inferred);
522                }
523
524                // Clear move state on reassignment
525                self.consumed_vars.remove(name);
526
527                // Lint: naming convention — variables should be snake_case
528                if !is_snake_case(name) {
529                    self.warnings.push(TypeError {
530                        message: format!("Variable `{name}` should be snake_case"),
531                        span: stmt.span,
532                        expected: None,
533                        found: None,
534                        hint: Some("Use lowercase with underscores for variable names".to_string()),
535                    });
536                }
537
538                // Lint: shadowing warning — check if variable already exists in outer scope
539                if !name.starts_with('_') && self.defined_vars.iter().any(|(n, _, _)| n == name) {
540                    self.warnings.push(TypeError {
541                        message: format!("Variable `{name}` shadows a previous definition"),
542                        span: stmt.span,
543                        expected: None,
544                        found: None,
545                        hint: Some("Consider using a different name".to_string()),
546                    });
547                }
548
549                // Track defined variable for unused-var checking
550                let depth = self.current_scope_depth();
551                self.defined_vars.push((name.clone(), stmt.span, depth));
552            }
553
554            StmtKind::LetDestructure { value, pattern, .. } => {
555                self.mark_used_in_expr(value);
556                self.mark_used_in_pattern(pattern);
557            }
558
559            StmtKind::FnDecl {
560                name,
561                type_params,
562                params,
563                return_type,
564                bounds,
565                body,
566                ..
567            } => {
568                // Lint: function naming convention — should be snake_case
569                if !is_snake_case(name) {
570                    self.warnings.push(TypeError {
571                        message: format!("Function `{name}` should be snake_case"),
572                        span: stmt.span,
573                        expected: None,
574                        found: None,
575                        hint: Some("Use lowercase with underscores for function names".to_string()),
576                    });
577                }
578
579                // Lint: empty function body
580                if body.is_empty() {
581                    self.warnings.push(TypeError {
582                        message: format!("Empty function body in `{name}`"),
583                        span: stmt.span,
584                        expected: None,
585                        found: None,
586                        hint: Some(
587                            "Consider adding an implementation or removing the function"
588                                .to_string(),
589                        ),
590                    });
591                }
592
593                // Save outer unused-var state
594                let outer_defined = std::mem::take(&mut self.defined_vars);
595                let outer_used = std::mem::take(&mut self.used_vars);
596
597                self.env.push_scope();
598
599                // Define type parameters in scope
600                for tp in type_params {
601                    self.env.define(tp.clone(), Type::TypeParam(tp.clone()));
602                }
603
604                // Validate trait bounds reference existing traits
605                for bound in bounds {
606                    for trait_name in &bound.traits {
607                        if self.env.lookup_trait(trait_name).is_none() {
608                            self.errors.push(TypeError {
609                                message: format!("Unknown trait `{trait_name}` in bound for `{}`", bound.type_param),
610                                span: stmt.span,
611                                expected: None,
612                                found: None,
613                                hint: Some("Available built-in traits: Numeric, Comparable, Hashable, Displayable, Serializable, Default".to_string()),
614                            });
615                        }
616                    }
617                    // Verify the bound references a declared type param
618                    if !type_params.contains(&bound.type_param) {
619                        self.errors.push(TypeError {
620                            message: format!(
621                                "Trait bound on undeclared type parameter `{}`",
622                                bound.type_param
623                            ),
624                            span: stmt.span,
625                            expected: None,
626                            found: None,
627                            hint: Some(format!(
628                                "Declare it in the type parameter list: `fn {}<{}, ...>(...)`",
629                                name, bound.type_param
630                            )),
631                        });
632                    }
633                }
634
635                // Bind parameters
636                let fn_depth = self.current_scope_depth();
637                for p in params {
638                    let ty = p
639                        .type_ann
640                        .as_ref()
641                        .map(|t| convert_type_expr_with_params(t, type_params))
642                        .unwrap_or(Type::Any);
643                    self.env.define(p.name.clone(), ty);
644                    // Track param as defined (for unused checking)
645                    self.defined_vars
646                        .push((p.name.clone(), stmt.span, fn_depth));
647
648                    // In strict mode, require type annotations on params
649                    if self.config.strict && p.type_ann.is_none() {
650                        self.errors.push(TypeError {
651                            message: format!(
652                                "Parameter `{}` of function `{name}` requires a type annotation in strict mode",
653                                p.name
654                            ),
655                            span: stmt.span,
656                            expected: None,
657                            found: None,
658                            hint: Some(format!("Add a type annotation: `{}: <type>`", p.name)),
659                        });
660                    }
661                }
662
663                // Set return type for checking returns
664                let prev_return = self.current_fn_return.take();
665                self.current_fn_return = return_type
666                    .as_ref()
667                    .map(|t| convert_type_expr_with_params(t, type_params));
668
669                // Track async state (Phase 24)
670                let prev_async = self.in_async_fn;
671                if let StmtKind::FnDecl { is_async, .. } = &stmt.kind
672                    && *is_async
673                {
674                    self.in_async_fn = true;
675                }
676
677                // Check body with unreachable code detection
678                self.check_body(body);
679
680                // Check unused vars in this function
681                self.check_unused_vars();
682
683                self.current_fn_return = prev_return;
684                self.in_async_fn = prev_async;
685                self.env.pop_scope();
686
687                // Restore outer unused-var state
688                self.defined_vars = outer_defined;
689                self.used_vars = outer_used;
690
691                // Mark the function name itself as used in the outer scope
692                self.used_vars.insert(name.clone());
693            }
694
695            StmtKind::Return(Some(expr)) => {
696                self.mark_used_in_expr(expr);
697                if let Some(expected_ret) = &self.current_fn_return {
698                    let inferred = infer_expr(expr, &self.env);
699                    if !is_compatible(expected_ret, &inferred) {
700                        self.errors.push(TypeError {
701                            message: "Return type mismatch".to_string(),
702                            span: stmt.span,
703                            expected: Some(expected_ret.to_string()),
704                            found: Some(inferred.to_string()),
705                            hint: None,
706                        });
707                    }
708                }
709            }
710
711            StmtKind::If {
712                condition,
713                then_body,
714                else_ifs,
715                else_body,
716            } => {
717                self.mark_used_in_expr(condition);
718                let cond_ty = infer_expr(condition, &self.env);
719                if !is_compatible(&Type::Bool, &cond_ty)
720                    && !matches!(cond_ty, Type::Any | Type::Error)
721                {
722                    self.warnings.push(TypeError {
723                        message: "Condition should be a bool".to_string(),
724                        span: stmt.span,
725                        expected: Some("bool".to_string()),
726                        found: Some(cond_ty.to_string()),
727                        hint: None,
728                    });
729                }
730                self.env.push_scope();
731                self.check_body(then_body);
732                self.env.pop_scope();
733
734                for (cond, body) in else_ifs {
735                    self.mark_used_in_expr(cond);
736                    let _ = infer_expr(cond, &self.env);
737                    self.env.push_scope();
738                    self.check_body(body);
739                    self.env.pop_scope();
740                }
741
742                if let Some(body) = else_body {
743                    self.env.push_scope();
744                    self.check_body(body);
745                    self.env.pop_scope();
746                }
747            }
748
749            StmtKind::While { condition, body } => {
750                self.mark_used_in_expr(condition);
751                let _ = infer_expr(condition, &self.env);
752                self.env.push_scope();
753                self.check_body(body);
754                self.env.pop_scope();
755            }
756
757            StmtKind::For { name, iter, body } | StmtKind::ParallelFor { name, iter, body } => {
758                self.mark_used_in_expr(iter);
759                let iter_ty = infer_expr(iter, &self.env);
760                let elem_ty = match &iter_ty {
761                    Type::List(inner) => *inner.clone(),
762                    Type::Set(inner) => *inner.clone(),
763                    Type::Generator(inner) => *inner.clone(),
764                    Type::Map(_) => Type::Any,
765                    Type::String => Type::String,
766                    Type::Any => Type::Any,
767                    _ => {
768                        self.warnings.push(TypeError {
769                            message: format!(
770                                "For-loop iterating over non-iterable type `{iter_ty}`"
771                            ),
772                            span: stmt.span,
773                            expected: Some("list, set, generator, map, or string".to_string()),
774                            found: Some(iter_ty.to_string()),
775                            hint: None,
776                        });
777                        Type::Any
778                    }
779                };
780                self.env.push_scope();
781                self.env.define(name.clone(), elem_ty);
782                // Mark loop variable as used — it's defined by the for-loop, not the user
783                self.used_vars.insert(name.clone());
784                self.check_body(body);
785                self.env.pop_scope();
786            }
787
788            StmtKind::Expr(expr) => {
789                self.mark_used_in_expr(expr);
790
791                // Check struct init field validation
792                if let Expr::StructInit { name, fields } = expr {
793                    self.check_struct_init(name, fields, stmt.span);
794                }
795
796                // Check assignment type compatibility
797                if let Expr::Assign { target, value } = expr {
798                    self.check_assignment(target, value, stmt.span);
799                }
800
801                // Check match exhaustiveness
802                self.check_match_exhaustiveness_in_expr(expr, stmt.span);
803
804                let _ = infer_expr(expr, &self.env);
805            }
806
807            StmtKind::TryCatch {
808                try_body,
809                catch_var,
810                catch_body,
811                finally_body,
812            } => {
813                self.env.push_scope();
814                self.check_body(try_body);
815                self.env.pop_scope();
816
817                self.env.push_scope();
818                self.env.define(catch_var.clone(), Type::Any);
819                self.used_vars.insert(catch_var.clone()); // catch vars are implicitly used
820                self.check_body(catch_body);
821                self.env.pop_scope();
822
823                if let Some(finally) = finally_body {
824                    self.env.push_scope();
825                    self.check_body(finally);
826                    self.env.pop_scope();
827                }
828            }
829
830            StmtKind::Throw(expr) => {
831                self.mark_used_in_expr(expr);
832                let _ = infer_expr(expr, &self.env);
833            }
834
835            StmtKind::ImplBlock { methods, .. } => {
836                for method in methods {
837                    self.check_stmt(method);
838                }
839            }
840
841            StmtKind::Test { body, .. } => {
842                self.env.push_scope();
843                self.check_body(body);
844                self.env.pop_scope();
845            }
846
847            StmtKind::Use { item, .. } => {
848                // Track imported names for unused import checking
849                match item {
850                    tl_ast::UseItem::Single(path) => {
851                        if let Some(last) = path.last() {
852                            self.imported_names.push((last.clone(), stmt.span));
853                        }
854                    }
855                    tl_ast::UseItem::Group(_, names) => {
856                        for name in names {
857                            self.imported_names.push((name.clone(), stmt.span));
858                        }
859                    }
860                    tl_ast::UseItem::Aliased(_, alias) => {
861                        self.imported_names.push((alias.clone(), stmt.span));
862                    }
863                    tl_ast::UseItem::Wildcard(_) => {} // can't check wildcard imports
864                }
865            }
866
867            StmtKind::StructDecl { name, .. } => {
868                // Lint: struct naming convention — should be PascalCase
869                if !is_pascal_case(name) {
870                    self.warnings.push(TypeError {
871                        message: format!("Struct `{name}` should be PascalCase"),
872                        span: stmt.span,
873                        expected: None,
874                        found: None,
875                        hint: Some("Use PascalCase for struct names".to_string()),
876                    });
877                }
878            }
879
880            StmtKind::EnumDecl { name, .. } => {
881                // Lint: enum naming convention — should be PascalCase
882                if !is_pascal_case(name) {
883                    self.warnings.push(TypeError {
884                        message: format!("Enum `{name}` should be PascalCase"),
885                        span: stmt.span,
886                        expected: None,
887                        found: None,
888                        hint: Some("Use PascalCase for enum names".to_string()),
889                    });
890                }
891            }
892
893            // Pass-through for statements we don't type check yet
894            StmtKind::Return(None)
895            | StmtKind::Break
896            | StmtKind::Continue
897            | StmtKind::Import { .. }
898            | StmtKind::Schema { .. }
899            | StmtKind::Train { .. }
900            | StmtKind::Pipeline { .. }
901            | StmtKind::StreamDecl { .. }
902            | StmtKind::SourceDecl { .. }
903            | StmtKind::SinkDecl { .. }
904            | StmtKind::ModDecl { .. }
905            | StmtKind::Migrate { .. }
906            | StmtKind::Agent { .. } => {}
907
908            StmtKind::TraitDef {
909                name,
910                type_params: _,
911                methods,
912                ..
913            } => {
914                // Lint: trait naming convention — should be PascalCase
915                if !is_pascal_case(name) {
916                    self.warnings.push(TypeError {
917                        message: format!("Trait `{name}` should be PascalCase"),
918                        span: stmt.span,
919                        expected: None,
920                        found: None,
921                        hint: Some("Use PascalCase for trait names".to_string()),
922                    });
923                }
924
925                // Register the trait
926                let method_sigs: Vec<(String, Vec<Type>, Type)> = methods
927                    .iter()
928                    .map(|m| {
929                        let param_types: Vec<Type> = m
930                            .params
931                            .iter()
932                            .map(|p| {
933                                p.type_ann
934                                    .as_ref()
935                                    .map(convert_type_expr)
936                                    .unwrap_or(Type::Any)
937                            })
938                            .collect();
939                        let ret = m
940                            .return_type
941                            .as_ref()
942                            .map(convert_type_expr)
943                            .unwrap_or(Type::Any);
944                        (m.name.clone(), param_types, ret)
945                    })
946                    .collect();
947                self.env.define_trait(
948                    name.clone(),
949                    TraitInfo {
950                        name: name.clone(),
951                        methods: method_sigs,
952                        supertrait: None,
953                    },
954                );
955            }
956
957            StmtKind::TraitImpl {
958                trait_name,
959                type_name,
960                methods,
961                ..
962            } => {
963                // Validate the trait exists
964                if let Some(trait_info) = self.env.lookup_trait(trait_name).cloned() {
965                    // Check all required methods are provided
966                    let provided: Vec<String> = methods
967                        .iter()
968                        .filter_map(|m| {
969                            if let StmtKind::FnDecl { name, .. } = &m.kind {
970                                Some(name.clone())
971                            } else {
972                                None
973                            }
974                        })
975                        .collect();
976
977                    for (required_method, _, _) in &trait_info.methods {
978                        if !provided.contains(required_method) {
979                            self.errors.push(TypeError {
980                                message: format!(
981                                    "Missing method `{required_method}` in impl `{trait_name}` for `{type_name}`"
982                                ),
983                                span: stmt.span,
984                                expected: None,
985                                found: None,
986                                hint: Some(format!("Trait `{trait_name}` requires method `{required_method}`")),
987                            });
988                        }
989                    }
990
991                    // Register the trait impl
992                    self.env
993                        .register_trait_impl(trait_name.clone(), type_name.clone(), provided);
994                } else {
995                    self.errors.push(TypeError {
996                        message: format!("Unknown trait `{trait_name}`"),
997                        span: stmt.span,
998                        expected: None,
999                        found: None,
1000                        hint: None,
1001                    });
1002                }
1003
1004                // Check method bodies
1005                for method in methods {
1006                    self.check_stmt(method);
1007                }
1008            }
1009
1010            StmtKind::TypeAlias {
1011                name,
1012                type_params,
1013                value,
1014                ..
1015            } => {
1016                // Register the type alias in the type environment
1017                self.env
1018                    .register_type_alias(name.clone(), type_params.clone(), value.clone());
1019            }
1020        }
1021    }
1022
1023    /// Validate struct initialization fields.
1024    fn check_struct_init(&mut self, name: &str, fields: &[(String, Expr)], span: Span) {
1025        if let Some(declared_fields) = self.env.lookup_struct(name).cloned() {
1026            // Check for unknown fields
1027            for (field_name, _) in fields {
1028                if !declared_fields.iter().any(|(f, _)| f == field_name) {
1029                    self.errors.push(TypeError {
1030                        message: format!("Unknown field `{field_name}` in struct `{name}`"),
1031                        span,
1032                        expected: None,
1033                        found: None,
1034                        hint: Some(format!(
1035                            "Available fields: {}",
1036                            declared_fields
1037                                .iter()
1038                                .map(|(f, _)| f.as_str())
1039                                .collect::<Vec<_>>()
1040                                .join(", ")
1041                        )),
1042                    });
1043                }
1044            }
1045
1046            // Check field types match
1047            for (field_name, field_value) in fields {
1048                if let Some((_, expected_ty)) =
1049                    declared_fields.iter().find(|(f, _)| f == field_name)
1050                {
1051                    let inferred = infer_expr(field_value, &self.env);
1052                    if !is_compatible(expected_ty, &inferred) {
1053                        self.errors.push(TypeError {
1054                            message: format!(
1055                                "Type mismatch for field `{field_name}` in struct `{name}`"
1056                            ),
1057                            span,
1058                            expected: Some(expected_ty.to_string()),
1059                            found: Some(inferred.to_string()),
1060                            hint: None,
1061                        });
1062                    }
1063                }
1064            }
1065        }
1066    }
1067
1068    /// Check assignment type compatibility.
1069    fn check_assignment(&mut self, target: &Expr, value: &Expr, span: Span) {
1070        let target_ty = infer_expr(target, &self.env);
1071        let value_ty = infer_expr(value, &self.env);
1072        // Only check if target has a known (non-Any) type
1073        if !matches!(target_ty, Type::Any | Type::Error) && !is_compatible(&target_ty, &value_ty) {
1074            self.warnings.push(TypeError {
1075                message: "Assignment type mismatch".to_string(),
1076                span,
1077                expected: Some(target_ty.to_string()),
1078                found: Some(value_ty.to_string()),
1079                hint: None,
1080            });
1081        }
1082    }
1083
1084    /// Check match expressions for exhaustiveness warnings.
1085    /// Check that a closure's return type annotation matches its body type.
1086    fn check_closure_return_type(&mut self, expr: &Expr, span: Span) {
1087        if let Expr::Closure {
1088            return_type: Some(rt),
1089            body,
1090            params,
1091            ..
1092        } = expr
1093        {
1094            let declared = convert_type_expr(rt);
1095            let body_type = match body {
1096                tl_ast::ClosureBody::Expr(e) => infer_expr(e, &self.env),
1097                tl_ast::ClosureBody::Block { expr: Some(e), .. } => infer_expr(e, &self.env),
1098                tl_ast::ClosureBody::Block { expr: None, .. } => Type::None,
1099            };
1100            if !is_compatible(&declared, &body_type) && !matches!(body_type, Type::Any) {
1101                self.warnings.push(TypeError {
1102                    message: "Closure return type mismatch".to_string(),
1103                    span,
1104                    expected: Some(declared.to_string()),
1105                    found: Some(body_type.to_string()),
1106                    hint: Some(
1107                        "The declared return type does not match the body expression type"
1108                            .to_string(),
1109                    ),
1110                });
1111            }
1112            // Warn on unused closure parameters (except _-prefixed)
1113            for p in params {
1114                if !p.name.starts_with('_') && p.name != "self" {
1115                    let is_used = match body {
1116                        tl_ast::ClosureBody::Expr(e) => self.expr_references_name(e, &p.name),
1117                        tl_ast::ClosureBody::Block { stmts, expr } => {
1118                            stmts.iter().any(|s| self.stmt_references_name(s, &p.name))
1119                                || expr
1120                                    .as_ref()
1121                                    .is_some_and(|e| self.expr_references_name(e, &p.name))
1122                        }
1123                    };
1124                    if !is_used {
1125                        self.warnings.push(TypeError {
1126                            message: format!("Unused closure parameter `{}`", p.name),
1127                            span,
1128                            expected: None,
1129                            found: None,
1130                            hint: Some(format!("Prefix with `_` to suppress: `_{}`", p.name)),
1131                        });
1132                    }
1133                }
1134            }
1135        }
1136    }
1137
1138    /// Check if an expression references a name (simple identifier check).
1139    fn expr_references_name(&self, expr: &Expr, name: &str) -> bool {
1140        match expr {
1141            Expr::Ident(n) => n == name,
1142            Expr::BinOp { left, right, .. } => {
1143                self.expr_references_name(left, name) || self.expr_references_name(right, name)
1144            }
1145            Expr::UnaryOp { expr: e, .. } => self.expr_references_name(e, name),
1146            Expr::Call { function, args } => {
1147                self.expr_references_name(function, name)
1148                    || args.iter().any(|a| self.expr_references_name(a, name))
1149            }
1150            Expr::Pipe { left, right } => {
1151                self.expr_references_name(left, name) || self.expr_references_name(right, name)
1152            }
1153            Expr::Member { object, .. } => self.expr_references_name(object, name),
1154            Expr::Index { object, index } => {
1155                self.expr_references_name(object, name) || self.expr_references_name(index, name)
1156            }
1157            Expr::List(items) => items.iter().any(|i| self.expr_references_name(i, name)),
1158            Expr::Map(pairs) => pairs.iter().any(|(k, v)| {
1159                self.expr_references_name(k, name) || self.expr_references_name(v, name)
1160            }),
1161            Expr::Block { stmts, expr } => {
1162                stmts.iter().any(|s| self.stmt_references_name(s, name))
1163                    || expr
1164                        .as_ref()
1165                        .is_some_and(|e| self.expr_references_name(e, name))
1166            }
1167            Expr::Assign { target, value } => {
1168                self.expr_references_name(target, name) || self.expr_references_name(value, name)
1169            }
1170            Expr::NullCoalesce { expr: e, default } => {
1171                self.expr_references_name(e, name) || self.expr_references_name(default, name)
1172            }
1173            Expr::Range { start, end } => {
1174                self.expr_references_name(start, name) || self.expr_references_name(end, name)
1175            }
1176            Expr::Await(e) | Expr::Try(e) => self.expr_references_name(e, name),
1177            Expr::NamedArg { value, .. } => self.expr_references_name(value, name),
1178            Expr::StructInit { fields, .. } => fields
1179                .iter()
1180                .any(|(_, e)| self.expr_references_name(e, name)),
1181            Expr::EnumVariant { args, .. } => {
1182                args.iter().any(|a| self.expr_references_name(a, name))
1183            }
1184            _ => false,
1185        }
1186    }
1187
1188    /// Check if a statement references a name.
1189    fn stmt_references_name(&self, stmt: &Stmt, name: &str) -> bool {
1190        match &stmt.kind {
1191            StmtKind::Expr(e) | StmtKind::Return(Some(e)) | StmtKind::Throw(e) => {
1192                self.expr_references_name(e, name)
1193            }
1194            StmtKind::Let { value, .. } | StmtKind::LetDestructure { value, .. } => {
1195                self.expr_references_name(value, name)
1196            }
1197            StmtKind::If {
1198                condition,
1199                then_body,
1200                else_ifs,
1201                else_body,
1202            } => {
1203                self.expr_references_name(condition, name)
1204                    || then_body.iter().any(|s| self.stmt_references_name(s, name))
1205                    || else_ifs.iter().any(|(c, b)| {
1206                        self.expr_references_name(c, name)
1207                            || b.iter().any(|s| self.stmt_references_name(s, name))
1208                    })
1209                    || else_body
1210                        .as_ref()
1211                        .is_some_and(|b| b.iter().any(|s| self.stmt_references_name(s, name)))
1212            }
1213            StmtKind::While { condition, body } => {
1214                self.expr_references_name(condition, name)
1215                    || body.iter().any(|s| self.stmt_references_name(s, name))
1216            }
1217            StmtKind::For { iter, body, .. } => {
1218                self.expr_references_name(iter, name)
1219                    || body.iter().any(|s| self.stmt_references_name(s, name))
1220            }
1221            _ => false,
1222        }
1223    }
1224
1225    fn check_match_exhaustiveness_in_expr(&mut self, expr: &Expr, span: Span) {
1226        if let Expr::Match { subject, arms } = expr {
1227            let subject_ty = infer_expr(subject, &self.env);
1228            let missing = check_match_exhaustiveness_patterns(&subject_ty, arms, &self.env);
1229            if !missing.is_empty() {
1230                self.warnings.push(TypeError {
1231                    message: format!("Non-exhaustive match: missing {}", missing.join(", ")),
1232                    span,
1233                    expected: None,
1234                    found: None,
1235                    hint: Some("Add missing patterns or a wildcard `_` arm".to_string()),
1236                });
1237            }
1238        }
1239    }
1240
1241    /// Emit warnings for unused variables.
1242    fn check_unused_vars(&mut self) {
1243        for (name, span, _depth) in &self.defined_vars {
1244            // Skip variables starting with _ (convention for intentionally unused)
1245            if name.starts_with('_') {
1246                continue;
1247            }
1248            if !self.used_vars.contains(name) {
1249                self.warnings.push(TypeError {
1250                    message: format!("Unused variable `{name}`"),
1251                    span: *span,
1252                    expected: None,
1253                    found: None,
1254                    hint: Some(format!("Prefix with `_` to suppress: `_{name}`")),
1255                });
1256            }
1257        }
1258    }
1259
1260    /// Emit warnings for unused imports.
1261    fn check_unused_imports(&mut self) {
1262        for (name, span) in &self.imported_names {
1263            if !self.used_imports.contains(name) {
1264                self.warnings.push(TypeError {
1265                    message: format!("Unused import `{name}`"),
1266                    span: *span,
1267                    expected: None,
1268                    found: None,
1269                    hint: Some("Remove unused import".to_string()),
1270                });
1271            }
1272        }
1273    }
1274}
1275
1276/// Check match arms for exhaustiveness on typed enums/result/option.
1277pub fn check_match_exhaustiveness(
1278    subject_type: &Type,
1279    arm_patterns: &[&str],
1280    env: &TypeEnv,
1281) -> Vec<String> {
1282    let mut missing = Vec::new();
1283
1284    match subject_type {
1285        Type::Result(_, _) => {
1286            if !arm_patterns.contains(&"Ok") {
1287                missing.push("Ok".to_string());
1288            }
1289            if !arm_patterns.contains(&"Err") {
1290                missing.push("Err".to_string());
1291            }
1292        }
1293        Type::Option(_) => {
1294            if !arm_patterns.iter().any(|p| *p == "none" || *p == "_") {
1295                missing.push("none".to_string());
1296            }
1297        }
1298        Type::Enum(name) => {
1299            if let Some(variants) = env.lookup_enum(name) {
1300                for (variant_name, _) in variants {
1301                    if !arm_patterns.iter().any(|p| p == variant_name || *p == "_") {
1302                        missing.push(variant_name.clone());
1303                    }
1304                }
1305            }
1306        }
1307        _ => {}
1308    }
1309
1310    missing
1311}
1312
1313/// Check match arms for exhaustiveness using Pattern types.
1314/// Returns a list of missing variant names, or empty if exhaustive.
1315pub fn check_match_exhaustiveness_patterns(
1316    subject_type: &Type,
1317    arms: &[MatchArm],
1318    env: &TypeEnv,
1319) -> Vec<String> {
1320    // If any arm is a wildcard or binding without guard, it's a catch-all
1321    let has_catch_all = arms.iter().any(|arm| {
1322        arm.guard.is_none() && matches!(arm.pattern, Pattern::Wildcard | Pattern::Binding(_))
1323    });
1324    if has_catch_all {
1325        return vec![];
1326    }
1327
1328    // Extract variant names from patterns
1329    let mut covered: Vec<&str> = Vec::new();
1330    for arm in arms {
1331        collect_pattern_variants(&arm.pattern, &mut covered);
1332    }
1333
1334    check_match_exhaustiveness(subject_type, &covered, env)
1335}
1336
1337/// Collect variant names covered by a pattern.
1338fn collect_pattern_variants<'a>(pattern: &'a Pattern, variants: &mut Vec<&'a str>) {
1339    match pattern {
1340        Pattern::Wildcard | Pattern::Binding(_) => {
1341            variants.push("_");
1342        }
1343        Pattern::Literal(Expr::None) => {
1344            variants.push("none");
1345        }
1346        Pattern::Enum { variant, .. } => {
1347            variants.push(variant.as_str());
1348        }
1349        Pattern::Or(pats) => {
1350            for p in pats {
1351                collect_pattern_variants(p, variants);
1352            }
1353        }
1354        _ => {}
1355    }
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360    use super::*;
1361
1362    fn parse_and_check(source: &str) -> CheckResult {
1363        let program = tl_parser::parse(source).unwrap();
1364        check_program(&program, &CheckerConfig::default())
1365    }
1366
1367    fn parse_and_check_strict(source: &str) -> CheckResult {
1368        let program = tl_parser::parse(source).unwrap();
1369        check_program(&program, &CheckerConfig { strict: true })
1370    }
1371
1372    #[test]
1373    fn test_correct_let_int() {
1374        let result = parse_and_check("let x: int = 42\nprint(x)");
1375        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1376    }
1377
1378    #[test]
1379    fn test_correct_let_string() {
1380        let result = parse_and_check("let s: string = \"hello\"\nprint(s)");
1381        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1382    }
1383
1384    #[test]
1385    fn test_mismatch_let() {
1386        let result = parse_and_check("let x: int = \"hello\"\nprint(x)");
1387        assert!(result.has_errors());
1388        assert!(result.errors[0].message.contains("mismatch"));
1389    }
1390
1391    #[test]
1392    fn test_gradual_untyped() {
1393        // Untyped code should always pass
1394        let result = parse_and_check("let x = 42\nlet y = \"hello\"\nprint(x)\nprint(y)");
1395        assert!(!result.has_errors());
1396    }
1397
1398    #[test]
1399    fn test_function_return_type() {
1400        let result = parse_and_check("fn f() -> int { return \"hello\" }");
1401        assert!(result.has_errors());
1402        assert!(result.errors[0].message.contains("Return type"));
1403    }
1404
1405    #[test]
1406    fn test_function_correct_return() {
1407        let result = parse_and_check("fn f() -> int { return 42 }");
1408        assert!(!result.has_errors());
1409    }
1410
1411    #[test]
1412    fn test_gradual_function_no_annotations() {
1413        let result = parse_and_check("fn f(a, b) { return a + b }");
1414        assert!(!result.has_errors());
1415    }
1416
1417    #[test]
1418    fn test_strict_mode_requires_param_types() {
1419        let result = parse_and_check_strict("fn f(a, b) { return a + b }");
1420        assert!(result.has_errors());
1421        assert!(
1422            result.errors[0]
1423                .message
1424                .contains("requires a type annotation")
1425        );
1426    }
1427
1428    #[test]
1429    fn test_strict_mode_with_annotations() {
1430        let result = parse_and_check_strict("fn f(a: int, b: int) -> int { return a + b }");
1431        assert!(!result.has_errors());
1432    }
1433
1434    #[test]
1435    fn test_option_none_compatible() {
1436        let mut env = TypeEnv::new();
1437        env.define("x".into(), Type::Option(Box::new(Type::Int)));
1438        assert!(is_compatible(
1439            &Type::Option(Box::new(Type::Int)),
1440            &Type::None
1441        ));
1442    }
1443
1444    #[test]
1445    fn test_int_float_promotion() {
1446        let result = parse_and_check("let x: float = 42\nprint(x)");
1447        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1448    }
1449
1450    #[test]
1451    fn test_match_exhaustiveness_result() {
1452        let env = TypeEnv::new();
1453        let ty = Type::Result(Box::new(Type::Int), Box::new(Type::String));
1454        let missing = check_match_exhaustiveness(&ty, &["Ok"], &env);
1455        assert_eq!(missing, vec!["Err"]);
1456
1457        let missing = check_match_exhaustiveness(&ty, &["Ok", "Err"], &env);
1458        assert!(missing.is_empty());
1459    }
1460
1461    #[test]
1462    fn test_match_exhaustiveness_enum() {
1463        let mut env = TypeEnv::new();
1464        env.define_enum(
1465            "Color".into(),
1466            vec![
1467                ("Red".into(), vec![]),
1468                ("Green".into(), vec![]),
1469                ("Blue".into(), vec![]),
1470            ],
1471        );
1472        let ty = Type::Enum("Color".into());
1473        let missing = check_match_exhaustiveness(&ty, &["Red", "Green"], &env);
1474        assert_eq!(missing, vec!["Blue"]);
1475    }
1476
1477    // ── Phase 12: Generics & Traits ──────────────────────────
1478
1479    #[test]
1480    fn test_generic_fn_type_params() {
1481        let result = parse_and_check("fn identity<T>(x: T) -> T { return x }");
1482        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1483    }
1484
1485    #[test]
1486    fn test_generic_fn_no_errors_untyped() {
1487        // Gradual: untyped generic code always passes
1488        let result = parse_and_check("fn identity<T>(x) { return x }");
1489        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1490    }
1491
1492    #[test]
1493    fn test_trait_def_registered() {
1494        let result = parse_and_check("trait Display { fn show(self) -> string }");
1495        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1496    }
1497
1498    #[test]
1499    fn test_trait_impl_validates_methods() {
1500        let result = parse_and_check(
1501            "trait Display { fn show(self) -> string }\nimpl Display for Point { fn show(self) -> string { \"point\" } }",
1502        );
1503        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1504    }
1505
1506    #[test]
1507    fn test_trait_impl_missing_method() {
1508        let result = parse_and_check(
1509            "trait Display { fn show(self) -> string }\nimpl Display for Point { fn other(self) { 1 } }",
1510        );
1511        assert!(result.has_errors());
1512        assert!(result.errors[0].message.contains("Missing method"));
1513    }
1514
1515    #[test]
1516    fn test_unknown_trait_in_impl() {
1517        let result = parse_and_check("impl FooTrait for Point { fn bar(self) { 1 } }");
1518        assert!(result.has_errors());
1519        assert!(result.errors[0].message.contains("Unknown trait"));
1520    }
1521
1522    #[test]
1523    fn test_unknown_trait_in_bound() {
1524        let result = parse_and_check("fn foo<T: UnknownTrait>(x: T) { x }");
1525        assert!(result.has_errors());
1526        assert!(result.errors[0].message.contains("Unknown trait"));
1527    }
1528
1529    #[test]
1530    fn test_builtin_trait_bound_accepted() {
1531        let result = parse_and_check("fn foo<T: Comparable>(x: T) { x }");
1532        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1533    }
1534
1535    #[test]
1536    fn test_multiple_bounds() {
1537        let result = parse_and_check("fn foo<T: Comparable + Hashable>(x: T) { x }");
1538        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1539    }
1540
1541    #[test]
1542    fn test_where_clause_validation() {
1543        let result = parse_and_check("fn foo<T>(x: T) where T: Comparable { x }");
1544        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1545    }
1546
1547    #[test]
1548    fn test_undeclared_type_param_in_bound() {
1549        let result = parse_and_check("fn foo<T>(x: T) where U: Comparable { x }");
1550        assert!(result.has_errors());
1551        assert!(
1552            result.errors[0]
1553                .message
1554                .contains("undeclared type parameter")
1555        );
1556    }
1557
1558    #[test]
1559    fn test_builtin_traits_registered() {
1560        let env = TypeEnv::new();
1561        assert!(env.lookup_trait("Numeric").is_some());
1562        assert!(env.lookup_trait("Comparable").is_some());
1563        assert!(env.lookup_trait("Hashable").is_some());
1564        assert!(env.lookup_trait("Displayable").is_some());
1565        assert!(env.lookup_trait("Serializable").is_some());
1566        assert!(env.lookup_trait("Default").is_some());
1567    }
1568
1569    #[test]
1570    fn test_type_satisfies_numeric() {
1571        let env = TypeEnv::new();
1572        assert!(env.type_satisfies_trait(&Type::Int, "Numeric"));
1573        assert!(env.type_satisfies_trait(&Type::Float, "Numeric"));
1574        assert!(!env.type_satisfies_trait(&Type::String, "Numeric"));
1575    }
1576
1577    #[test]
1578    fn test_type_satisfies_comparable() {
1579        let env = TypeEnv::new();
1580        assert!(env.type_satisfies_trait(&Type::Int, "Comparable"));
1581        assert!(env.type_satisfies_trait(&Type::String, "Comparable"));
1582        assert!(!env.type_satisfies_trait(&Type::Bool, "Comparable"));
1583    }
1584
1585    #[test]
1586    fn test_strict_mode_with_generics() {
1587        // In strict mode, params still need annotations — type params count as annotations
1588        let result = parse_and_check_strict("fn identity<T>(x: T) -> T { return x }");
1589        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1590    }
1591
1592    // ── Phase 13: Enhanced Checking ──────────────────────────
1593
1594    #[test]
1595    fn test_struct_init_correct_fields() {
1596        let result = parse_and_check(
1597            "struct Point { x: int, y: int }\nlet p = Point { x: 1, y: 2 }\nprint(p)",
1598        );
1599        assert!(!result.has_errors(), "errors: {:?}", result.errors);
1600    }
1601
1602    #[test]
1603    fn test_struct_init_unknown_field() {
1604        let result = parse_and_check(
1605            "struct Point { x: int, y: int }\nlet p = Point { x: 1, z: 2 }\nprint(p)",
1606        );
1607        assert!(result.has_errors());
1608        assert!(result.errors[0].message.contains("Unknown field `z`"));
1609    }
1610
1611    #[test]
1612    fn test_struct_init_wrong_field_type() {
1613        let result = parse_and_check(
1614            "struct Point { x: int, y: int }\nlet p = Point { x: 1, y: \"hello\" }\nprint(p)",
1615        );
1616        assert!(result.has_errors());
1617        assert!(
1618            result.errors[0]
1619                .message
1620                .contains("Type mismatch for field `y`")
1621        );
1622    }
1623
1624    #[test]
1625    fn test_assignment_type_mismatch() {
1626        let result = parse_and_check("let mut x: int = 42\nx = \"hello\"");
1627        // Assignment type mismatch is a warning in gradual mode
1628        let assign_warnings: Vec<_> = result
1629            .warnings
1630            .iter()
1631            .filter(|w| w.message.contains("Assignment type mismatch"))
1632            .collect();
1633        assert!(
1634            !assign_warnings.is_empty(),
1635            "Expected assignment type mismatch warning. warnings: {:?}",
1636            result.warnings
1637        );
1638    }
1639
1640    #[test]
1641    fn test_unused_variable_warning() {
1642        let result = parse_and_check("let x = 42");
1643        let unused_warnings: Vec<_> = result
1644            .warnings
1645            .iter()
1646            .filter(|w| w.message.contains("Unused variable `x`"))
1647            .collect();
1648        assert!(
1649            !unused_warnings.is_empty(),
1650            "Expected unused variable warning. warnings: {:?}",
1651            result.warnings
1652        );
1653    }
1654
1655    #[test]
1656    fn test_underscore_prefix_no_warning() {
1657        let result = parse_and_check("let _x = 42");
1658        let unused_warnings: Vec<_> = result
1659            .warnings
1660            .iter()
1661            .filter(|w| w.message.contains("Unused variable"))
1662            .collect();
1663        assert!(
1664            unused_warnings.is_empty(),
1665            "Should not warn for _-prefixed variables. warnings: {:?}",
1666            result.warnings
1667        );
1668    }
1669
1670    #[test]
1671    fn test_used_variable_no_warning() {
1672        let result = parse_and_check("let x = 42\nprint(x)");
1673        let unused_warnings: Vec<_> = result
1674            .warnings
1675            .iter()
1676            .filter(|w| w.message.contains("Unused variable `x`"))
1677            .collect();
1678        assert!(
1679            unused_warnings.is_empty(),
1680            "Should not warn for used variables. warnings: {:?}",
1681            result.warnings
1682        );
1683    }
1684
1685    #[test]
1686    fn test_unreachable_code_after_return() {
1687        let result = parse_and_check("fn f() {\n  return 1\n  print(\"unreachable\")\n}");
1688        let unreachable: Vec<_> = result
1689            .warnings
1690            .iter()
1691            .filter(|w| w.message.contains("Unreachable code"))
1692            .collect();
1693        assert!(
1694            !unreachable.is_empty(),
1695            "Expected unreachable code warning. warnings: {:?}",
1696            result.warnings
1697        );
1698    }
1699
1700    #[test]
1701    fn test_unreachable_code_after_break() {
1702        let result =
1703            parse_and_check("fn f() {\n  while true {\n    break\n    print(\"x\")\n  }\n}");
1704        let unreachable: Vec<_> = result
1705            .warnings
1706            .iter()
1707            .filter(|w| w.message.contains("Unreachable code"))
1708            .collect();
1709        assert!(
1710            !unreachable.is_empty(),
1711            "Expected unreachable code warning after break. warnings: {:?}",
1712            result.warnings
1713        );
1714    }
1715
1716    #[test]
1717    fn test_for_loop_non_iterable_warning() {
1718        let result = parse_and_check("let x: int = 42\nfor i in x { print(i) }");
1719        let warnings: Vec<_> = result
1720            .warnings
1721            .iter()
1722            .filter(|w| w.message.contains("non-iterable"))
1723            .collect();
1724        assert!(
1725            !warnings.is_empty(),
1726            "Expected non-iterable warning. warnings: {:?}",
1727            result.warnings
1728        );
1729    }
1730
1731    #[test]
1732    fn test_multiple_warnings_accumulated() {
1733        let result = parse_and_check("let x = 42\nlet y = 43");
1734        let unused_warnings: Vec<_> = result
1735            .warnings
1736            .iter()
1737            .filter(|w| w.message.contains("Unused variable"))
1738            .collect();
1739        assert_eq!(
1740            unused_warnings.len(),
1741            2,
1742            "Expected 2 unused variable warnings. warnings: {:?}",
1743            result.warnings
1744        );
1745    }
1746
1747    #[test]
1748    fn test_all_existing_patterns_pass() {
1749        // Verify various existing patterns still work without errors
1750        let result = parse_and_check("fn f(a, b) { return a + b }");
1751        assert!(!result.has_errors());
1752
1753        let result = parse_and_check("let xs = [1, 2, 3]\nprint(xs)");
1754        assert!(!result.has_errors());
1755    }
1756
1757    // ── Phase 14: Lint Rules ──────────────────────────
1758
1759    #[test]
1760    fn test_snake_case_function_no_warning() {
1761        let result = parse_and_check("fn my_func() { 1 }");
1762        let naming: Vec<_> = result
1763            .warnings
1764            .iter()
1765            .filter(|w| w.message.contains("snake_case"))
1766            .collect();
1767        assert!(
1768            naming.is_empty(),
1769            "snake_case function should not produce naming warning"
1770        );
1771    }
1772
1773    #[test]
1774    fn test_camel_case_function_warning() {
1775        let result = parse_and_check("fn myFunc() { 1 }");
1776        let naming: Vec<_> = result
1777            .warnings
1778            .iter()
1779            .filter(|w| w.message.contains("snake_case"))
1780            .collect();
1781        assert!(
1782            !naming.is_empty(),
1783            "camelCase function should produce naming warning"
1784        );
1785    }
1786
1787    #[test]
1788    fn test_pascal_case_struct_no_warning() {
1789        let result = parse_and_check("struct MyStruct { x: int }");
1790        let naming: Vec<_> = result
1791            .warnings
1792            .iter()
1793            .filter(|w| w.message.contains("PascalCase"))
1794            .collect();
1795        assert!(
1796            naming.is_empty(),
1797            "PascalCase struct should not produce naming warning"
1798        );
1799    }
1800
1801    #[test]
1802    fn test_lowercase_struct_warning() {
1803        let result = parse_and_check("struct my_struct { x: int }");
1804        let naming: Vec<_> = result
1805            .warnings
1806            .iter()
1807            .filter(|w| w.message.contains("PascalCase"))
1808            .collect();
1809        assert!(
1810            !naming.is_empty(),
1811            "lowercase struct should produce naming warning"
1812        );
1813    }
1814
1815    #[test]
1816    fn test_variable_shadowing_warning() {
1817        let result = parse_and_check("let x = 1\nlet x = 2\nprint(x)");
1818        let shadow: Vec<_> = result
1819            .warnings
1820            .iter()
1821            .filter(|w| w.message.contains("shadows"))
1822            .collect();
1823        assert!(
1824            !shadow.is_empty(),
1825            "Shadowed variable should produce warning: {:?}",
1826            result.warnings
1827        );
1828    }
1829
1830    #[test]
1831    fn test_underscore_shadow_no_warning() {
1832        let result = parse_and_check("let _x = 1\nlet _x = 2");
1833        let shadow: Vec<_> = result
1834            .warnings
1835            .iter()
1836            .filter(|w| w.message.contains("shadows"))
1837            .collect();
1838        assert!(
1839            shadow.is_empty(),
1840            "_-prefixed shadow should not warn: {:?}",
1841            result.warnings
1842        );
1843    }
1844
1845    // ── Phase 17: Pattern Matching ──
1846
1847    #[test]
1848    fn test_match_exhaustiveness_patterns_with_wildcard() {
1849        let env = TypeEnv::new();
1850        let ty = Type::Result(Box::new(Type::Int), Box::new(Type::String));
1851        let arms = vec![
1852            MatchArm {
1853                pattern: Pattern::Enum {
1854                    type_name: "Result".into(),
1855                    variant: "Ok".into(),
1856                    args: vec![Pattern::Binding("v".into())],
1857                },
1858                guard: None,
1859                body: Expr::Ident("v".into()),
1860            },
1861            MatchArm {
1862                pattern: Pattern::Wildcard,
1863                guard: None,
1864                body: Expr::None,
1865            },
1866        ];
1867        let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1868        assert!(missing.is_empty(), "Wildcard should make match exhaustive");
1869    }
1870
1871    #[test]
1872    fn test_match_exhaustiveness_patterns_missing_variant() {
1873        let mut env = TypeEnv::new();
1874        env.define_enum(
1875            "Color".into(),
1876            vec![
1877                ("Red".into(), vec![]),
1878                ("Green".into(), vec![]),
1879                ("Blue".into(), vec![]),
1880            ],
1881        );
1882        let ty = Type::Enum("Color".into());
1883        let arms = vec![
1884            MatchArm {
1885                pattern: Pattern::Enum {
1886                    type_name: "Color".into(),
1887                    variant: "Red".into(),
1888                    args: vec![],
1889                },
1890                guard: None,
1891                body: Expr::Int(1),
1892            },
1893            MatchArm {
1894                pattern: Pattern::Enum {
1895                    type_name: "Color".into(),
1896                    variant: "Green".into(),
1897                    args: vec![],
1898                },
1899                guard: None,
1900                body: Expr::Int(2),
1901            },
1902        ];
1903        let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1904        assert_eq!(missing, vec!["Blue"]);
1905    }
1906
1907    #[test]
1908    fn test_match_exhaustiveness_patterns_all_variants() {
1909        let mut env = TypeEnv::new();
1910        env.define_enum(
1911            "Color".into(),
1912            vec![
1913                ("Red".into(), vec![]),
1914                ("Green".into(), vec![]),
1915                ("Blue".into(), vec![]),
1916            ],
1917        );
1918        let ty = Type::Enum("Color".into());
1919        let arms = vec![
1920            MatchArm {
1921                pattern: Pattern::Enum {
1922                    type_name: "Color".into(),
1923                    variant: "Red".into(),
1924                    args: vec![],
1925                },
1926                guard: None,
1927                body: Expr::Int(1),
1928            },
1929            MatchArm {
1930                pattern: Pattern::Enum {
1931                    type_name: "Color".into(),
1932                    variant: "Green".into(),
1933                    args: vec![],
1934                },
1935                guard: None,
1936                body: Expr::Int(2),
1937            },
1938            MatchArm {
1939                pattern: Pattern::Enum {
1940                    type_name: "Color".into(),
1941                    variant: "Blue".into(),
1942                    args: vec![],
1943                },
1944                guard: None,
1945                body: Expr::Int(3),
1946            },
1947        ];
1948        let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1949        assert!(missing.is_empty());
1950    }
1951
1952    #[test]
1953    fn test_match_exhaustiveness_binding_is_catchall() {
1954        let env = TypeEnv::new();
1955        let ty = Type::Result(Box::new(Type::Int), Box::new(Type::String));
1956        let arms = vec![MatchArm {
1957            pattern: Pattern::Binding("x".into()),
1958            guard: None,
1959            body: Expr::Ident("x".into()),
1960        }];
1961        let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1962        assert!(missing.is_empty(), "Binding pattern should be a catch-all");
1963    }
1964
1965    #[test]
1966    fn test_match_exhaustiveness_or_pattern() {
1967        let mut env = TypeEnv::new();
1968        env.define_enum(
1969            "Dir".into(),
1970            vec![
1971                ("North".into(), vec![]),
1972                ("South".into(), vec![]),
1973                ("East".into(), vec![]),
1974                ("West".into(), vec![]),
1975            ],
1976        );
1977        let ty = Type::Enum("Dir".into());
1978        let arms = vec![
1979            MatchArm {
1980                pattern: Pattern::Or(vec![
1981                    Pattern::Enum {
1982                        type_name: "Dir".into(),
1983                        variant: "North".into(),
1984                        args: vec![],
1985                    },
1986                    Pattern::Enum {
1987                        type_name: "Dir".into(),
1988                        variant: "South".into(),
1989                        args: vec![],
1990                    },
1991                ]),
1992                guard: None,
1993                body: Expr::String("vertical".into()),
1994            },
1995            MatchArm {
1996                pattern: Pattern::Or(vec![
1997                    Pattern::Enum {
1998                        type_name: "Dir".into(),
1999                        variant: "East".into(),
2000                        args: vec![],
2001                    },
2002                    Pattern::Enum {
2003                        type_name: "Dir".into(),
2004                        variant: "West".into(),
2005                        args: vec![],
2006                    },
2007                ]),
2008                guard: None,
2009                body: Expr::String("horizontal".into()),
2010            },
2011        ];
2012        let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
2013        assert!(missing.is_empty(), "OR patterns should cover all variants");
2014    }
2015
2016    // ── Phase 18: Closure type checking ────────────────
2017
2018    #[test]
2019    fn test_checker_closure_annotated_params() {
2020        let src = "let _f = (x: int64) => x * 2";
2021        let program = tl_parser::parse(src).unwrap();
2022        let config = CheckerConfig::default();
2023        let result = check_program(&program, &config);
2024        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
2025    }
2026
2027    #[test]
2028    fn test_checker_block_body_closure_type_inferred() {
2029        let src = "let _f = (x: int64) -> int64 { let _y = x * 2\n _y + 1 }";
2030        let program = tl_parser::parse(src).unwrap();
2031        let config = CheckerConfig::default();
2032        let result = check_program(&program, &config);
2033        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
2034    }
2035
2036    #[test]
2037    fn test_checker_unused_closure_param_warning() {
2038        let src = "let _f = (x: int64) -> int64 { 42 }";
2039        let program = tl_parser::parse(src).unwrap();
2040        let config = CheckerConfig::default();
2041        let result = check_program(&program, &config);
2042        let has_unused_warning = result
2043            .warnings
2044            .iter()
2045            .any(|w| w.message.contains("Unused closure parameter"));
2046        assert!(
2047            has_unused_warning,
2048            "Expected unused closure parameter warning, got: {:?}",
2049            result.warnings
2050        );
2051    }
2052
2053    #[test]
2054    fn test_checker_closure_return_type_mismatch() {
2055        // Closure declares int64 return type but body returns a string
2056        let src = "let _f = (x: int64) -> int64 { \"hello\" }";
2057        let program = tl_parser::parse(src).unwrap();
2058        let config = CheckerConfig::default();
2059        let result = check_program(&program, &config);
2060        let has_mismatch = result
2061            .warnings
2062            .iter()
2063            .any(|w| w.message.contains("return type mismatch"));
2064        assert!(
2065            has_mismatch,
2066            "Expected return type mismatch warning, got: {:?}",
2067            result.warnings
2068        );
2069    }
2070
2071    // ── Phase 22-24 Checker Tests ──────────────────────────────────
2072
2073    #[test]
2074    fn test_checker_decimal_type_annotation() {
2075        let src = "let _x: decimal = 1.0d";
2076        let program = tl_parser::parse(src).unwrap();
2077        let config = CheckerConfig::default();
2078        let result = check_program(&program, &config);
2079        assert!(
2080            !result.has_errors(),
2081            "Decimal type annotation should be valid: {:?}",
2082            result.errors
2083        );
2084    }
2085
2086    #[test]
2087    fn test_checker_async_fn_parses() {
2088        let src = "async fn _fetch() { return 42 }";
2089        let program = tl_parser::parse(src).unwrap();
2090        let config = CheckerConfig::default();
2091        let result = check_program(&program, &config);
2092        assert!(
2093            !result.has_errors(),
2094            "async fn should type-check without errors: {:?}",
2095            result.errors
2096        );
2097    }
2098
2099    #[test]
2100    fn test_checker_await_outside_async_warns() {
2101        let src = r#"
2102fn _sync_fn() {
2103    let _t = spawn(() => 42)
2104    let _x = await _t
2105}
2106"#;
2107        let program = tl_parser::parse(src).unwrap();
2108        let config = CheckerConfig::default();
2109        let result = check_program(&program, &config);
2110        let has_await_warn = result
2111            .warnings
2112            .iter()
2113            .any(|w| w.message.contains("await") && w.message.contains("async"));
2114        assert!(
2115            has_await_warn,
2116            "Expected await-outside-async warning, got: {:?}",
2117            result.warnings
2118        );
2119    }
2120
2121    #[test]
2122    fn test_checker_await_inside_async_no_warn() {
2123        let src = r#"
2124async fn _async_fn() {
2125    let _t = spawn(() => 42)
2126    let _x = await _t
2127}
2128"#;
2129        let program = tl_parser::parse(src).unwrap();
2130        let config = CheckerConfig::default();
2131        let result = check_program(&program, &config);
2132        let has_await_warn = result
2133            .warnings
2134            .iter()
2135            .any(|w| w.message.contains("await") && w.message.contains("async"));
2136        assert!(
2137            !has_await_warn,
2138            "Should not warn about await inside async fn, but got: {:?}",
2139            result.warnings
2140        );
2141    }
2142}