1use 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#[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#[derive(Default)]
37pub struct CheckerConfig {
38 pub strict: bool,
40}
41
42pub 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
54pub 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 for stmt in &program.statements {
72 checker.register_top_level(stmt);
73 }
74
75 checker.check_body(&program.statements);
77
78 checker.check_unused_vars();
80
81 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 current_fn_return: Option<Type>,
97 defined_vars: Vec<(String, Span, u32)>,
99 used_vars: HashSet<String>,
101 imported_names: Vec<(String, Span)>,
103 used_imports: HashSet<String>,
105 in_async_fn: bool,
107 consumed_vars: std::collections::HashMap<String, Span>,
109}
110
111pub fn is_snake_case(s: &str) -> bool {
113 if s.is_empty() || s.starts_with('_') {
114 return true; }
116 s.chars()
117 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
118}
119
120pub 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
129fn 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 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 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 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 return;
268 }
269 self.check_stmt(stmt);
270 if is_terminator(&stmt.kind) {
271 terminated = true;
272 }
273 }
274 }
275
276 #[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 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 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 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 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 self.mark_used_in_expr(value);
491
492 if let Expr::StructInit {
494 name: sname,
495 fields,
496 } = value
497 {
498 self.check_struct_init(sname, fields, stmt.span);
499 }
500
501 self.check_match_exhaustiveness_in_expr(value, stmt.span);
503
504 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 self.consumed_vars.remove(name);
526
527 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 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 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 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 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 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 for tp in type_params {
601 self.env.define(tp.clone(), Type::TypeParam(tp.clone()));
602 }
603
604 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 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 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 self.defined_vars
646 .push((p.name.clone(), stmt.span, fn_depth));
647
648 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 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 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 self.check_body(body);
679
680 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 self.defined_vars = outer_defined;
689 self.used_vars = outer_used;
690
691 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 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 if let Expr::StructInit { name, fields } = expr {
793 self.check_struct_init(name, fields, stmt.span);
794 }
795
796 if let Expr::Assign { target, value } = expr {
798 self.check_assignment(target, value, stmt.span);
799 }
800
801 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 } => {
812 self.env.push_scope();
813 self.check_body(try_body);
814 self.env.pop_scope();
815
816 self.env.push_scope();
817 self.env.define(catch_var.clone(), Type::Any);
818 self.used_vars.insert(catch_var.clone()); self.check_body(catch_body);
820 self.env.pop_scope();
821 }
822
823 StmtKind::Throw(expr) => {
824 self.mark_used_in_expr(expr);
825 let _ = infer_expr(expr, &self.env);
826 }
827
828 StmtKind::ImplBlock { methods, .. } => {
829 for method in methods {
830 self.check_stmt(method);
831 }
832 }
833
834 StmtKind::Test { body, .. } => {
835 self.env.push_scope();
836 self.check_body(body);
837 self.env.pop_scope();
838 }
839
840 StmtKind::Use { item, .. } => {
841 match item {
843 tl_ast::UseItem::Single(path) => {
844 if let Some(last) = path.last() {
845 self.imported_names.push((last.clone(), stmt.span));
846 }
847 }
848 tl_ast::UseItem::Group(_, names) => {
849 for name in names {
850 self.imported_names.push((name.clone(), stmt.span));
851 }
852 }
853 tl_ast::UseItem::Aliased(_, alias) => {
854 self.imported_names.push((alias.clone(), stmt.span));
855 }
856 tl_ast::UseItem::Wildcard(_) => {} }
858 }
859
860 StmtKind::StructDecl { name, .. } => {
861 if !is_pascal_case(name) {
863 self.warnings.push(TypeError {
864 message: format!("Struct `{name}` should be PascalCase"),
865 span: stmt.span,
866 expected: None,
867 found: None,
868 hint: Some("Use PascalCase for struct names".to_string()),
869 });
870 }
871 }
872
873 StmtKind::EnumDecl { name, .. } => {
874 if !is_pascal_case(name) {
876 self.warnings.push(TypeError {
877 message: format!("Enum `{name}` should be PascalCase"),
878 span: stmt.span,
879 expected: None,
880 found: None,
881 hint: Some("Use PascalCase for enum names".to_string()),
882 });
883 }
884 }
885
886 StmtKind::Return(None)
888 | StmtKind::Break
889 | StmtKind::Continue
890 | StmtKind::Import { .. }
891 | StmtKind::Schema { .. }
892 | StmtKind::Train { .. }
893 | StmtKind::Pipeline { .. }
894 | StmtKind::StreamDecl { .. }
895 | StmtKind::SourceDecl { .. }
896 | StmtKind::SinkDecl { .. }
897 | StmtKind::ModDecl { .. }
898 | StmtKind::Migrate { .. } => {}
899
900 StmtKind::TraitDef {
901 name,
902 type_params: _,
903 methods,
904 ..
905 } => {
906 if !is_pascal_case(name) {
908 self.warnings.push(TypeError {
909 message: format!("Trait `{name}` should be PascalCase"),
910 span: stmt.span,
911 expected: None,
912 found: None,
913 hint: Some("Use PascalCase for trait names".to_string()),
914 });
915 }
916
917 let method_sigs: Vec<(String, Vec<Type>, Type)> = methods
919 .iter()
920 .map(|m| {
921 let param_types: Vec<Type> = m
922 .params
923 .iter()
924 .map(|p| {
925 p.type_ann
926 .as_ref()
927 .map(convert_type_expr)
928 .unwrap_or(Type::Any)
929 })
930 .collect();
931 let ret = m
932 .return_type
933 .as_ref()
934 .map(convert_type_expr)
935 .unwrap_or(Type::Any);
936 (m.name.clone(), param_types, ret)
937 })
938 .collect();
939 self.env.define_trait(
940 name.clone(),
941 TraitInfo {
942 name: name.clone(),
943 methods: method_sigs,
944 supertrait: None,
945 },
946 );
947 }
948
949 StmtKind::TraitImpl {
950 trait_name,
951 type_name,
952 methods,
953 ..
954 } => {
955 if let Some(trait_info) = self.env.lookup_trait(trait_name).cloned() {
957 let provided: Vec<String> = methods
959 .iter()
960 .filter_map(|m| {
961 if let StmtKind::FnDecl { name, .. } = &m.kind {
962 Some(name.clone())
963 } else {
964 None
965 }
966 })
967 .collect();
968
969 for (required_method, _, _) in &trait_info.methods {
970 if !provided.contains(required_method) {
971 self.errors.push(TypeError {
972 message: format!(
973 "Missing method `{required_method}` in impl `{trait_name}` for `{type_name}`"
974 ),
975 span: stmt.span,
976 expected: None,
977 found: None,
978 hint: Some(format!("Trait `{trait_name}` requires method `{required_method}`")),
979 });
980 }
981 }
982
983 self.env
985 .register_trait_impl(trait_name.clone(), type_name.clone(), provided);
986 } else {
987 self.errors.push(TypeError {
988 message: format!("Unknown trait `{trait_name}`"),
989 span: stmt.span,
990 expected: None,
991 found: None,
992 hint: None,
993 });
994 }
995
996 for method in methods {
998 self.check_stmt(method);
999 }
1000 }
1001
1002 StmtKind::TypeAlias {
1003 name,
1004 type_params,
1005 value,
1006 ..
1007 } => {
1008 self.env
1010 .register_type_alias(name.clone(), type_params.clone(), value.clone());
1011 }
1012 }
1013 }
1014
1015 fn check_struct_init(&mut self, name: &str, fields: &[(String, Expr)], span: Span) {
1017 if let Some(declared_fields) = self.env.lookup_struct(name).cloned() {
1018 for (field_name, _) in fields {
1020 if !declared_fields.iter().any(|(f, _)| f == field_name) {
1021 self.errors.push(TypeError {
1022 message: format!("Unknown field `{field_name}` in struct `{name}`"),
1023 span,
1024 expected: None,
1025 found: None,
1026 hint: Some(format!(
1027 "Available fields: {}",
1028 declared_fields
1029 .iter()
1030 .map(|(f, _)| f.as_str())
1031 .collect::<Vec<_>>()
1032 .join(", ")
1033 )),
1034 });
1035 }
1036 }
1037
1038 for (field_name, field_value) in fields {
1040 if let Some((_, expected_ty)) =
1041 declared_fields.iter().find(|(f, _)| f == field_name)
1042 {
1043 let inferred = infer_expr(field_value, &self.env);
1044 if !is_compatible(expected_ty, &inferred) {
1045 self.errors.push(TypeError {
1046 message: format!(
1047 "Type mismatch for field `{field_name}` in struct `{name}`"
1048 ),
1049 span,
1050 expected: Some(expected_ty.to_string()),
1051 found: Some(inferred.to_string()),
1052 hint: None,
1053 });
1054 }
1055 }
1056 }
1057 }
1058 }
1059
1060 fn check_assignment(&mut self, target: &Expr, value: &Expr, span: Span) {
1062 let target_ty = infer_expr(target, &self.env);
1063 let value_ty = infer_expr(value, &self.env);
1064 if !matches!(target_ty, Type::Any | Type::Error) && !is_compatible(&target_ty, &value_ty) {
1066 self.warnings.push(TypeError {
1067 message: "Assignment type mismatch".to_string(),
1068 span,
1069 expected: Some(target_ty.to_string()),
1070 found: Some(value_ty.to_string()),
1071 hint: None,
1072 });
1073 }
1074 }
1075
1076 fn check_closure_return_type(&mut self, expr: &Expr, span: Span) {
1079 if let Expr::Closure {
1080 return_type: Some(rt),
1081 body,
1082 params,
1083 ..
1084 } = expr
1085 {
1086 let declared = convert_type_expr(rt);
1087 let body_type = match body {
1088 tl_ast::ClosureBody::Expr(e) => infer_expr(e, &self.env),
1089 tl_ast::ClosureBody::Block { expr: Some(e), .. } => infer_expr(e, &self.env),
1090 tl_ast::ClosureBody::Block { expr: None, .. } => Type::None,
1091 };
1092 if !is_compatible(&declared, &body_type) && !matches!(body_type, Type::Any) {
1093 self.warnings.push(TypeError {
1094 message: "Closure return type mismatch".to_string(),
1095 span,
1096 expected: Some(declared.to_string()),
1097 found: Some(body_type.to_string()),
1098 hint: Some(
1099 "The declared return type does not match the body expression type"
1100 .to_string(),
1101 ),
1102 });
1103 }
1104 for p in params {
1106 if !p.name.starts_with('_') && p.name != "self" {
1107 let is_used = match body {
1108 tl_ast::ClosureBody::Expr(e) => self.expr_references_name(e, &p.name),
1109 tl_ast::ClosureBody::Block { stmts, expr } => {
1110 stmts.iter().any(|s| self.stmt_references_name(s, &p.name))
1111 || expr
1112 .as_ref()
1113 .is_some_and(|e| self.expr_references_name(e, &p.name))
1114 }
1115 };
1116 if !is_used {
1117 self.warnings.push(TypeError {
1118 message: format!("Unused closure parameter `{}`", p.name),
1119 span,
1120 expected: None,
1121 found: None,
1122 hint: Some(format!("Prefix with `_` to suppress: `_{}`", p.name)),
1123 });
1124 }
1125 }
1126 }
1127 }
1128 }
1129
1130 fn expr_references_name(&self, expr: &Expr, name: &str) -> bool {
1132 match expr {
1133 Expr::Ident(n) => n == name,
1134 Expr::BinOp { left, right, .. } => {
1135 self.expr_references_name(left, name) || self.expr_references_name(right, name)
1136 }
1137 Expr::UnaryOp { expr: e, .. } => self.expr_references_name(e, name),
1138 Expr::Call { function, args } => {
1139 self.expr_references_name(function, name)
1140 || args.iter().any(|a| self.expr_references_name(a, name))
1141 }
1142 Expr::Pipe { left, right } => {
1143 self.expr_references_name(left, name) || self.expr_references_name(right, name)
1144 }
1145 Expr::Member { object, .. } => self.expr_references_name(object, name),
1146 Expr::Index { object, index } => {
1147 self.expr_references_name(object, name) || self.expr_references_name(index, name)
1148 }
1149 Expr::List(items) => items.iter().any(|i| self.expr_references_name(i, name)),
1150 Expr::Map(pairs) => pairs.iter().any(|(k, v)| {
1151 self.expr_references_name(k, name) || self.expr_references_name(v, name)
1152 }),
1153 Expr::Block { stmts, expr } => {
1154 stmts.iter().any(|s| self.stmt_references_name(s, name))
1155 || expr
1156 .as_ref()
1157 .is_some_and(|e| self.expr_references_name(e, name))
1158 }
1159 Expr::Assign { target, value } => {
1160 self.expr_references_name(target, name) || self.expr_references_name(value, name)
1161 }
1162 Expr::NullCoalesce { expr: e, default } => {
1163 self.expr_references_name(e, name) || self.expr_references_name(default, name)
1164 }
1165 Expr::Range { start, end } => {
1166 self.expr_references_name(start, name) || self.expr_references_name(end, name)
1167 }
1168 Expr::Await(e) | Expr::Try(e) => self.expr_references_name(e, name),
1169 Expr::NamedArg { value, .. } => self.expr_references_name(value, name),
1170 Expr::StructInit { fields, .. } => fields
1171 .iter()
1172 .any(|(_, e)| self.expr_references_name(e, name)),
1173 Expr::EnumVariant { args, .. } => {
1174 args.iter().any(|a| self.expr_references_name(a, name))
1175 }
1176 _ => false,
1177 }
1178 }
1179
1180 fn stmt_references_name(&self, stmt: &Stmt, name: &str) -> bool {
1182 match &stmt.kind {
1183 StmtKind::Expr(e) | StmtKind::Return(Some(e)) | StmtKind::Throw(e) => {
1184 self.expr_references_name(e, name)
1185 }
1186 StmtKind::Let { value, .. } | StmtKind::LetDestructure { value, .. } => {
1187 self.expr_references_name(value, name)
1188 }
1189 StmtKind::If {
1190 condition,
1191 then_body,
1192 else_ifs,
1193 else_body,
1194 } => {
1195 self.expr_references_name(condition, name)
1196 || then_body.iter().any(|s| self.stmt_references_name(s, name))
1197 || else_ifs.iter().any(|(c, b)| {
1198 self.expr_references_name(c, name)
1199 || b.iter().any(|s| self.stmt_references_name(s, name))
1200 })
1201 || else_body
1202 .as_ref()
1203 .is_some_and(|b| b.iter().any(|s| self.stmt_references_name(s, name)))
1204 }
1205 StmtKind::While { condition, body } => {
1206 self.expr_references_name(condition, name)
1207 || body.iter().any(|s| self.stmt_references_name(s, name))
1208 }
1209 StmtKind::For { iter, body, .. } => {
1210 self.expr_references_name(iter, name)
1211 || body.iter().any(|s| self.stmt_references_name(s, name))
1212 }
1213 _ => false,
1214 }
1215 }
1216
1217 fn check_match_exhaustiveness_in_expr(&mut self, expr: &Expr, span: Span) {
1218 if let Expr::Match { subject, arms } = expr {
1219 let subject_ty = infer_expr(subject, &self.env);
1220 let missing = check_match_exhaustiveness_patterns(&subject_ty, arms, &self.env);
1221 if !missing.is_empty() {
1222 self.warnings.push(TypeError {
1223 message: format!("Non-exhaustive match: missing {}", missing.join(", ")),
1224 span,
1225 expected: None,
1226 found: None,
1227 hint: Some("Add missing patterns or a wildcard `_` arm".to_string()),
1228 });
1229 }
1230 }
1231 }
1232
1233 fn check_unused_vars(&mut self) {
1235 for (name, span, _depth) in &self.defined_vars {
1236 if name.starts_with('_') {
1238 continue;
1239 }
1240 if !self.used_vars.contains(name) {
1241 self.warnings.push(TypeError {
1242 message: format!("Unused variable `{name}`"),
1243 span: *span,
1244 expected: None,
1245 found: None,
1246 hint: Some(format!("Prefix with `_` to suppress: `_{name}`")),
1247 });
1248 }
1249 }
1250 }
1251
1252 fn check_unused_imports(&mut self) {
1254 for (name, span) in &self.imported_names {
1255 if !self.used_imports.contains(name) {
1256 self.warnings.push(TypeError {
1257 message: format!("Unused import `{name}`"),
1258 span: *span,
1259 expected: None,
1260 found: None,
1261 hint: Some("Remove unused import".to_string()),
1262 });
1263 }
1264 }
1265 }
1266}
1267
1268pub fn check_match_exhaustiveness(
1270 subject_type: &Type,
1271 arm_patterns: &[&str],
1272 env: &TypeEnv,
1273) -> Vec<String> {
1274 let mut missing = Vec::new();
1275
1276 match subject_type {
1277 Type::Result(_, _) => {
1278 if !arm_patterns.contains(&"Ok") {
1279 missing.push("Ok".to_string());
1280 }
1281 if !arm_patterns.contains(&"Err") {
1282 missing.push("Err".to_string());
1283 }
1284 }
1285 Type::Option(_) => {
1286 if !arm_patterns.iter().any(|p| *p == "none" || *p == "_") {
1287 missing.push("none".to_string());
1288 }
1289 }
1290 Type::Enum(name) => {
1291 if let Some(variants) = env.lookup_enum(name) {
1292 for (variant_name, _) in variants {
1293 if !arm_patterns.iter().any(|p| p == variant_name || *p == "_") {
1294 missing.push(variant_name.clone());
1295 }
1296 }
1297 }
1298 }
1299 _ => {}
1300 }
1301
1302 missing
1303}
1304
1305pub fn check_match_exhaustiveness_patterns(
1308 subject_type: &Type,
1309 arms: &[MatchArm],
1310 env: &TypeEnv,
1311) -> Vec<String> {
1312 let has_catch_all = arms.iter().any(|arm| {
1314 arm.guard.is_none() && matches!(arm.pattern, Pattern::Wildcard | Pattern::Binding(_))
1315 });
1316 if has_catch_all {
1317 return vec![];
1318 }
1319
1320 let mut covered: Vec<&str> = Vec::new();
1322 for arm in arms {
1323 collect_pattern_variants(&arm.pattern, &mut covered);
1324 }
1325
1326 check_match_exhaustiveness(subject_type, &covered, env)
1327}
1328
1329fn collect_pattern_variants<'a>(pattern: &'a Pattern, variants: &mut Vec<&'a str>) {
1331 match pattern {
1332 Pattern::Wildcard | Pattern::Binding(_) => {
1333 variants.push("_");
1334 }
1335 Pattern::Literal(Expr::None) => {
1336 variants.push("none");
1337 }
1338 Pattern::Enum { variant, .. } => {
1339 variants.push(variant.as_str());
1340 }
1341 Pattern::Or(pats) => {
1342 for p in pats {
1343 collect_pattern_variants(p, variants);
1344 }
1345 }
1346 _ => {}
1347 }
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352 use super::*;
1353
1354 fn parse_and_check(source: &str) -> CheckResult {
1355 let program = tl_parser::parse(source).unwrap();
1356 check_program(&program, &CheckerConfig::default())
1357 }
1358
1359 fn parse_and_check_strict(source: &str) -> CheckResult {
1360 let program = tl_parser::parse(source).unwrap();
1361 check_program(&program, &CheckerConfig { strict: true })
1362 }
1363
1364 #[test]
1365 fn test_correct_let_int() {
1366 let result = parse_and_check("let x: int = 42\nprint(x)");
1367 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1368 }
1369
1370 #[test]
1371 fn test_correct_let_string() {
1372 let result = parse_and_check("let s: string = \"hello\"\nprint(s)");
1373 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1374 }
1375
1376 #[test]
1377 fn test_mismatch_let() {
1378 let result = parse_and_check("let x: int = \"hello\"\nprint(x)");
1379 assert!(result.has_errors());
1380 assert!(result.errors[0].message.contains("mismatch"));
1381 }
1382
1383 #[test]
1384 fn test_gradual_untyped() {
1385 let result = parse_and_check("let x = 42\nlet y = \"hello\"\nprint(x)\nprint(y)");
1387 assert!(!result.has_errors());
1388 }
1389
1390 #[test]
1391 fn test_function_return_type() {
1392 let result = parse_and_check("fn f() -> int { return \"hello\" }");
1393 assert!(result.has_errors());
1394 assert!(result.errors[0].message.contains("Return type"));
1395 }
1396
1397 #[test]
1398 fn test_function_correct_return() {
1399 let result = parse_and_check("fn f() -> int { return 42 }");
1400 assert!(!result.has_errors());
1401 }
1402
1403 #[test]
1404 fn test_gradual_function_no_annotations() {
1405 let result = parse_and_check("fn f(a, b) { return a + b }");
1406 assert!(!result.has_errors());
1407 }
1408
1409 #[test]
1410 fn test_strict_mode_requires_param_types() {
1411 let result = parse_and_check_strict("fn f(a, b) { return a + b }");
1412 assert!(result.has_errors());
1413 assert!(
1414 result.errors[0]
1415 .message
1416 .contains("requires a type annotation")
1417 );
1418 }
1419
1420 #[test]
1421 fn test_strict_mode_with_annotations() {
1422 let result = parse_and_check_strict("fn f(a: int, b: int) -> int { return a + b }");
1423 assert!(!result.has_errors());
1424 }
1425
1426 #[test]
1427 fn test_option_none_compatible() {
1428 let mut env = TypeEnv::new();
1429 env.define("x".into(), Type::Option(Box::new(Type::Int)));
1430 assert!(is_compatible(
1431 &Type::Option(Box::new(Type::Int)),
1432 &Type::None
1433 ));
1434 }
1435
1436 #[test]
1437 fn test_int_float_promotion() {
1438 let result = parse_and_check("let x: float = 42\nprint(x)");
1439 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1440 }
1441
1442 #[test]
1443 fn test_match_exhaustiveness_result() {
1444 let env = TypeEnv::new();
1445 let ty = Type::Result(Box::new(Type::Int), Box::new(Type::String));
1446 let missing = check_match_exhaustiveness(&ty, &["Ok"], &env);
1447 assert_eq!(missing, vec!["Err"]);
1448
1449 let missing = check_match_exhaustiveness(&ty, &["Ok", "Err"], &env);
1450 assert!(missing.is_empty());
1451 }
1452
1453 #[test]
1454 fn test_match_exhaustiveness_enum() {
1455 let mut env = TypeEnv::new();
1456 env.define_enum(
1457 "Color".into(),
1458 vec![
1459 ("Red".into(), vec![]),
1460 ("Green".into(), vec![]),
1461 ("Blue".into(), vec![]),
1462 ],
1463 );
1464 let ty = Type::Enum("Color".into());
1465 let missing = check_match_exhaustiveness(&ty, &["Red", "Green"], &env);
1466 assert_eq!(missing, vec!["Blue"]);
1467 }
1468
1469 #[test]
1472 fn test_generic_fn_type_params() {
1473 let result = parse_and_check("fn identity<T>(x: T) -> T { return x }");
1474 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1475 }
1476
1477 #[test]
1478 fn test_generic_fn_no_errors_untyped() {
1479 let result = parse_and_check("fn identity<T>(x) { return x }");
1481 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1482 }
1483
1484 #[test]
1485 fn test_trait_def_registered() {
1486 let result = parse_and_check("trait Display { fn show(self) -> string }");
1487 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1488 }
1489
1490 #[test]
1491 fn test_trait_impl_validates_methods() {
1492 let result = parse_and_check(
1493 "trait Display { fn show(self) -> string }\nimpl Display for Point { fn show(self) -> string { \"point\" } }",
1494 );
1495 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1496 }
1497
1498 #[test]
1499 fn test_trait_impl_missing_method() {
1500 let result = parse_and_check(
1501 "trait Display { fn show(self) -> string }\nimpl Display for Point { fn other(self) { 1 } }",
1502 );
1503 assert!(result.has_errors());
1504 assert!(result.errors[0].message.contains("Missing method"));
1505 }
1506
1507 #[test]
1508 fn test_unknown_trait_in_impl() {
1509 let result = parse_and_check("impl FooTrait for Point { fn bar(self) { 1 } }");
1510 assert!(result.has_errors());
1511 assert!(result.errors[0].message.contains("Unknown trait"));
1512 }
1513
1514 #[test]
1515 fn test_unknown_trait_in_bound() {
1516 let result = parse_and_check("fn foo<T: UnknownTrait>(x: T) { x }");
1517 assert!(result.has_errors());
1518 assert!(result.errors[0].message.contains("Unknown trait"));
1519 }
1520
1521 #[test]
1522 fn test_builtin_trait_bound_accepted() {
1523 let result = parse_and_check("fn foo<T: Comparable>(x: T) { x }");
1524 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1525 }
1526
1527 #[test]
1528 fn test_multiple_bounds() {
1529 let result = parse_and_check("fn foo<T: Comparable + Hashable>(x: T) { x }");
1530 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1531 }
1532
1533 #[test]
1534 fn test_where_clause_validation() {
1535 let result = parse_and_check("fn foo<T>(x: T) where T: Comparable { x }");
1536 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1537 }
1538
1539 #[test]
1540 fn test_undeclared_type_param_in_bound() {
1541 let result = parse_and_check("fn foo<T>(x: T) where U: Comparable { x }");
1542 assert!(result.has_errors());
1543 assert!(
1544 result.errors[0]
1545 .message
1546 .contains("undeclared type parameter")
1547 );
1548 }
1549
1550 #[test]
1551 fn test_builtin_traits_registered() {
1552 let env = TypeEnv::new();
1553 assert!(env.lookup_trait("Numeric").is_some());
1554 assert!(env.lookup_trait("Comparable").is_some());
1555 assert!(env.lookup_trait("Hashable").is_some());
1556 assert!(env.lookup_trait("Displayable").is_some());
1557 assert!(env.lookup_trait("Serializable").is_some());
1558 assert!(env.lookup_trait("Default").is_some());
1559 }
1560
1561 #[test]
1562 fn test_type_satisfies_numeric() {
1563 let env = TypeEnv::new();
1564 assert!(env.type_satisfies_trait(&Type::Int, "Numeric"));
1565 assert!(env.type_satisfies_trait(&Type::Float, "Numeric"));
1566 assert!(!env.type_satisfies_trait(&Type::String, "Numeric"));
1567 }
1568
1569 #[test]
1570 fn test_type_satisfies_comparable() {
1571 let env = TypeEnv::new();
1572 assert!(env.type_satisfies_trait(&Type::Int, "Comparable"));
1573 assert!(env.type_satisfies_trait(&Type::String, "Comparable"));
1574 assert!(!env.type_satisfies_trait(&Type::Bool, "Comparable"));
1575 }
1576
1577 #[test]
1578 fn test_strict_mode_with_generics() {
1579 let result = parse_and_check_strict("fn identity<T>(x: T) -> T { return x }");
1581 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1582 }
1583
1584 #[test]
1587 fn test_struct_init_correct_fields() {
1588 let result = parse_and_check(
1589 "struct Point { x: int, y: int }\nlet p = Point { x: 1, y: 2 }\nprint(p)",
1590 );
1591 assert!(!result.has_errors(), "errors: {:?}", result.errors);
1592 }
1593
1594 #[test]
1595 fn test_struct_init_unknown_field() {
1596 let result = parse_and_check(
1597 "struct Point { x: int, y: int }\nlet p = Point { x: 1, z: 2 }\nprint(p)",
1598 );
1599 assert!(result.has_errors());
1600 assert!(result.errors[0].message.contains("Unknown field `z`"));
1601 }
1602
1603 #[test]
1604 fn test_struct_init_wrong_field_type() {
1605 let result = parse_and_check(
1606 "struct Point { x: int, y: int }\nlet p = Point { x: 1, y: \"hello\" }\nprint(p)",
1607 );
1608 assert!(result.has_errors());
1609 assert!(
1610 result.errors[0]
1611 .message
1612 .contains("Type mismatch for field `y`")
1613 );
1614 }
1615
1616 #[test]
1617 fn test_assignment_type_mismatch() {
1618 let result = parse_and_check("let mut x: int = 42\nx = \"hello\"");
1619 let assign_warnings: Vec<_> = result
1621 .warnings
1622 .iter()
1623 .filter(|w| w.message.contains("Assignment type mismatch"))
1624 .collect();
1625 assert!(
1626 !assign_warnings.is_empty(),
1627 "Expected assignment type mismatch warning. warnings: {:?}",
1628 result.warnings
1629 );
1630 }
1631
1632 #[test]
1633 fn test_unused_variable_warning() {
1634 let result = parse_and_check("let x = 42");
1635 let unused_warnings: Vec<_> = result
1636 .warnings
1637 .iter()
1638 .filter(|w| w.message.contains("Unused variable `x`"))
1639 .collect();
1640 assert!(
1641 !unused_warnings.is_empty(),
1642 "Expected unused variable warning. warnings: {:?}",
1643 result.warnings
1644 );
1645 }
1646
1647 #[test]
1648 fn test_underscore_prefix_no_warning() {
1649 let result = parse_and_check("let _x = 42");
1650 let unused_warnings: Vec<_> = result
1651 .warnings
1652 .iter()
1653 .filter(|w| w.message.contains("Unused variable"))
1654 .collect();
1655 assert!(
1656 unused_warnings.is_empty(),
1657 "Should not warn for _-prefixed variables. warnings: {:?}",
1658 result.warnings
1659 );
1660 }
1661
1662 #[test]
1663 fn test_used_variable_no_warning() {
1664 let result = parse_and_check("let x = 42\nprint(x)");
1665 let unused_warnings: Vec<_> = result
1666 .warnings
1667 .iter()
1668 .filter(|w| w.message.contains("Unused variable `x`"))
1669 .collect();
1670 assert!(
1671 unused_warnings.is_empty(),
1672 "Should not warn for used variables. warnings: {:?}",
1673 result.warnings
1674 );
1675 }
1676
1677 #[test]
1678 fn test_unreachable_code_after_return() {
1679 let result = parse_and_check("fn f() {\n return 1\n print(\"unreachable\")\n}");
1680 let unreachable: Vec<_> = result
1681 .warnings
1682 .iter()
1683 .filter(|w| w.message.contains("Unreachable code"))
1684 .collect();
1685 assert!(
1686 !unreachable.is_empty(),
1687 "Expected unreachable code warning. warnings: {:?}",
1688 result.warnings
1689 );
1690 }
1691
1692 #[test]
1693 fn test_unreachable_code_after_break() {
1694 let result =
1695 parse_and_check("fn f() {\n while true {\n break\n print(\"x\")\n }\n}");
1696 let unreachable: Vec<_> = result
1697 .warnings
1698 .iter()
1699 .filter(|w| w.message.contains("Unreachable code"))
1700 .collect();
1701 assert!(
1702 !unreachable.is_empty(),
1703 "Expected unreachable code warning after break. warnings: {:?}",
1704 result.warnings
1705 );
1706 }
1707
1708 #[test]
1709 fn test_for_loop_non_iterable_warning() {
1710 let result = parse_and_check("let x: int = 42\nfor i in x { print(i) }");
1711 let warnings: Vec<_> = result
1712 .warnings
1713 .iter()
1714 .filter(|w| w.message.contains("non-iterable"))
1715 .collect();
1716 assert!(
1717 !warnings.is_empty(),
1718 "Expected non-iterable warning. warnings: {:?}",
1719 result.warnings
1720 );
1721 }
1722
1723 #[test]
1724 fn test_multiple_warnings_accumulated() {
1725 let result = parse_and_check("let x = 42\nlet y = 43");
1726 let unused_warnings: Vec<_> = result
1727 .warnings
1728 .iter()
1729 .filter(|w| w.message.contains("Unused variable"))
1730 .collect();
1731 assert_eq!(
1732 unused_warnings.len(),
1733 2,
1734 "Expected 2 unused variable warnings. warnings: {:?}",
1735 result.warnings
1736 );
1737 }
1738
1739 #[test]
1740 fn test_all_existing_patterns_pass() {
1741 let result = parse_and_check("fn f(a, b) { return a + b }");
1743 assert!(!result.has_errors());
1744
1745 let result = parse_and_check("let xs = [1, 2, 3]\nprint(xs)");
1746 assert!(!result.has_errors());
1747 }
1748
1749 #[test]
1752 fn test_snake_case_function_no_warning() {
1753 let result = parse_and_check("fn my_func() { 1 }");
1754 let naming: Vec<_> = result
1755 .warnings
1756 .iter()
1757 .filter(|w| w.message.contains("snake_case"))
1758 .collect();
1759 assert!(
1760 naming.is_empty(),
1761 "snake_case function should not produce naming warning"
1762 );
1763 }
1764
1765 #[test]
1766 fn test_camel_case_function_warning() {
1767 let result = parse_and_check("fn myFunc() { 1 }");
1768 let naming: Vec<_> = result
1769 .warnings
1770 .iter()
1771 .filter(|w| w.message.contains("snake_case"))
1772 .collect();
1773 assert!(
1774 !naming.is_empty(),
1775 "camelCase function should produce naming warning"
1776 );
1777 }
1778
1779 #[test]
1780 fn test_pascal_case_struct_no_warning() {
1781 let result = parse_and_check("struct MyStruct { x: int }");
1782 let naming: Vec<_> = result
1783 .warnings
1784 .iter()
1785 .filter(|w| w.message.contains("PascalCase"))
1786 .collect();
1787 assert!(
1788 naming.is_empty(),
1789 "PascalCase struct should not produce naming warning"
1790 );
1791 }
1792
1793 #[test]
1794 fn test_lowercase_struct_warning() {
1795 let result = parse_and_check("struct my_struct { x: int }");
1796 let naming: Vec<_> = result
1797 .warnings
1798 .iter()
1799 .filter(|w| w.message.contains("PascalCase"))
1800 .collect();
1801 assert!(
1802 !naming.is_empty(),
1803 "lowercase struct should produce naming warning"
1804 );
1805 }
1806
1807 #[test]
1808 fn test_variable_shadowing_warning() {
1809 let result = parse_and_check("let x = 1\nlet x = 2\nprint(x)");
1810 let shadow: Vec<_> = result
1811 .warnings
1812 .iter()
1813 .filter(|w| w.message.contains("shadows"))
1814 .collect();
1815 assert!(
1816 !shadow.is_empty(),
1817 "Shadowed variable should produce warning: {:?}",
1818 result.warnings
1819 );
1820 }
1821
1822 #[test]
1823 fn test_underscore_shadow_no_warning() {
1824 let result = parse_and_check("let _x = 1\nlet _x = 2");
1825 let shadow: Vec<_> = result
1826 .warnings
1827 .iter()
1828 .filter(|w| w.message.contains("shadows"))
1829 .collect();
1830 assert!(
1831 shadow.is_empty(),
1832 "_-prefixed shadow should not warn: {:?}",
1833 result.warnings
1834 );
1835 }
1836
1837 #[test]
1840 fn test_match_exhaustiveness_patterns_with_wildcard() {
1841 let env = TypeEnv::new();
1842 let ty = Type::Result(Box::new(Type::Int), Box::new(Type::String));
1843 let arms = vec![
1844 MatchArm {
1845 pattern: Pattern::Enum {
1846 type_name: "Result".into(),
1847 variant: "Ok".into(),
1848 args: vec![Pattern::Binding("v".into())],
1849 },
1850 guard: None,
1851 body: Expr::Ident("v".into()),
1852 },
1853 MatchArm {
1854 pattern: Pattern::Wildcard,
1855 guard: None,
1856 body: Expr::None,
1857 },
1858 ];
1859 let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1860 assert!(missing.is_empty(), "Wildcard should make match exhaustive");
1861 }
1862
1863 #[test]
1864 fn test_match_exhaustiveness_patterns_missing_variant() {
1865 let mut env = TypeEnv::new();
1866 env.define_enum(
1867 "Color".into(),
1868 vec![
1869 ("Red".into(), vec![]),
1870 ("Green".into(), vec![]),
1871 ("Blue".into(), vec![]),
1872 ],
1873 );
1874 let ty = Type::Enum("Color".into());
1875 let arms = vec![
1876 MatchArm {
1877 pattern: Pattern::Enum {
1878 type_name: "Color".into(),
1879 variant: "Red".into(),
1880 args: vec![],
1881 },
1882 guard: None,
1883 body: Expr::Int(1),
1884 },
1885 MatchArm {
1886 pattern: Pattern::Enum {
1887 type_name: "Color".into(),
1888 variant: "Green".into(),
1889 args: vec![],
1890 },
1891 guard: None,
1892 body: Expr::Int(2),
1893 },
1894 ];
1895 let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1896 assert_eq!(missing, vec!["Blue"]);
1897 }
1898
1899 #[test]
1900 fn test_match_exhaustiveness_patterns_all_variants() {
1901 let mut env = TypeEnv::new();
1902 env.define_enum(
1903 "Color".into(),
1904 vec![
1905 ("Red".into(), vec![]),
1906 ("Green".into(), vec![]),
1907 ("Blue".into(), vec![]),
1908 ],
1909 );
1910 let ty = Type::Enum("Color".into());
1911 let arms = vec![
1912 MatchArm {
1913 pattern: Pattern::Enum {
1914 type_name: "Color".into(),
1915 variant: "Red".into(),
1916 args: vec![],
1917 },
1918 guard: None,
1919 body: Expr::Int(1),
1920 },
1921 MatchArm {
1922 pattern: Pattern::Enum {
1923 type_name: "Color".into(),
1924 variant: "Green".into(),
1925 args: vec![],
1926 },
1927 guard: None,
1928 body: Expr::Int(2),
1929 },
1930 MatchArm {
1931 pattern: Pattern::Enum {
1932 type_name: "Color".into(),
1933 variant: "Blue".into(),
1934 args: vec![],
1935 },
1936 guard: None,
1937 body: Expr::Int(3),
1938 },
1939 ];
1940 let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1941 assert!(missing.is_empty());
1942 }
1943
1944 #[test]
1945 fn test_match_exhaustiveness_binding_is_catchall() {
1946 let env = TypeEnv::new();
1947 let ty = Type::Result(Box::new(Type::Int), Box::new(Type::String));
1948 let arms = vec![MatchArm {
1949 pattern: Pattern::Binding("x".into()),
1950 guard: None,
1951 body: Expr::Ident("x".into()),
1952 }];
1953 let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
1954 assert!(missing.is_empty(), "Binding pattern should be a catch-all");
1955 }
1956
1957 #[test]
1958 fn test_match_exhaustiveness_or_pattern() {
1959 let mut env = TypeEnv::new();
1960 env.define_enum(
1961 "Dir".into(),
1962 vec![
1963 ("North".into(), vec![]),
1964 ("South".into(), vec![]),
1965 ("East".into(), vec![]),
1966 ("West".into(), vec![]),
1967 ],
1968 );
1969 let ty = Type::Enum("Dir".into());
1970 let arms = vec![
1971 MatchArm {
1972 pattern: Pattern::Or(vec![
1973 Pattern::Enum {
1974 type_name: "Dir".into(),
1975 variant: "North".into(),
1976 args: vec![],
1977 },
1978 Pattern::Enum {
1979 type_name: "Dir".into(),
1980 variant: "South".into(),
1981 args: vec![],
1982 },
1983 ]),
1984 guard: None,
1985 body: Expr::String("vertical".into()),
1986 },
1987 MatchArm {
1988 pattern: Pattern::Or(vec![
1989 Pattern::Enum {
1990 type_name: "Dir".into(),
1991 variant: "East".into(),
1992 args: vec![],
1993 },
1994 Pattern::Enum {
1995 type_name: "Dir".into(),
1996 variant: "West".into(),
1997 args: vec![],
1998 },
1999 ]),
2000 guard: None,
2001 body: Expr::String("horizontal".into()),
2002 },
2003 ];
2004 let missing = check_match_exhaustiveness_patterns(&ty, &arms, &env);
2005 assert!(missing.is_empty(), "OR patterns should cover all variants");
2006 }
2007
2008 #[test]
2011 fn test_checker_closure_annotated_params() {
2012 let src = "let _f = (x: int64) => x * 2";
2013 let program = tl_parser::parse(src).unwrap();
2014 let config = CheckerConfig::default();
2015 let result = check_program(&program, &config);
2016 assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
2017 }
2018
2019 #[test]
2020 fn test_checker_block_body_closure_type_inferred() {
2021 let src = "let _f = (x: int64) -> int64 { let _y = x * 2\n _y + 1 }";
2022 let program = tl_parser::parse(src).unwrap();
2023 let config = CheckerConfig::default();
2024 let result = check_program(&program, &config);
2025 assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
2026 }
2027
2028 #[test]
2029 fn test_checker_unused_closure_param_warning() {
2030 let src = "let _f = (x: int64) -> int64 { 42 }";
2031 let program = tl_parser::parse(src).unwrap();
2032 let config = CheckerConfig::default();
2033 let result = check_program(&program, &config);
2034 let has_unused_warning = result
2035 .warnings
2036 .iter()
2037 .any(|w| w.message.contains("Unused closure parameter"));
2038 assert!(
2039 has_unused_warning,
2040 "Expected unused closure parameter warning, got: {:?}",
2041 result.warnings
2042 );
2043 }
2044
2045 #[test]
2046 fn test_checker_closure_return_type_mismatch() {
2047 let src = "let _f = (x: int64) -> int64 { \"hello\" }";
2049 let program = tl_parser::parse(src).unwrap();
2050 let config = CheckerConfig::default();
2051 let result = check_program(&program, &config);
2052 let has_mismatch = result
2053 .warnings
2054 .iter()
2055 .any(|w| w.message.contains("return type mismatch"));
2056 assert!(
2057 has_mismatch,
2058 "Expected return type mismatch warning, got: {:?}",
2059 result.warnings
2060 );
2061 }
2062
2063 #[test]
2066 fn test_checker_decimal_type_annotation() {
2067 let src = "let _x: decimal = 1.0d";
2068 let program = tl_parser::parse(src).unwrap();
2069 let config = CheckerConfig::default();
2070 let result = check_program(&program, &config);
2071 assert!(
2072 !result.has_errors(),
2073 "Decimal type annotation should be valid: {:?}",
2074 result.errors
2075 );
2076 }
2077
2078 #[test]
2079 fn test_checker_async_fn_parses() {
2080 let src = "async fn _fetch() { return 42 }";
2081 let program = tl_parser::parse(src).unwrap();
2082 let config = CheckerConfig::default();
2083 let result = check_program(&program, &config);
2084 assert!(
2085 !result.has_errors(),
2086 "async fn should type-check without errors: {:?}",
2087 result.errors
2088 );
2089 }
2090
2091 #[test]
2092 fn test_checker_await_outside_async_warns() {
2093 let src = r#"
2094fn _sync_fn() {
2095 let _t = spawn(() => 42)
2096 let _x = await _t
2097}
2098"#;
2099 let program = tl_parser::parse(src).unwrap();
2100 let config = CheckerConfig::default();
2101 let result = check_program(&program, &config);
2102 let has_await_warn = result
2103 .warnings
2104 .iter()
2105 .any(|w| w.message.contains("await") && w.message.contains("async"));
2106 assert!(
2107 has_await_warn,
2108 "Expected await-outside-async warning, got: {:?}",
2109 result.warnings
2110 );
2111 }
2112
2113 #[test]
2114 fn test_checker_await_inside_async_no_warn() {
2115 let src = r#"
2116async fn _async_fn() {
2117 let _t = spawn(() => 42)
2118 let _x = await _t
2119}
2120"#;
2121 let program = tl_parser::parse(src).unwrap();
2122 let config = CheckerConfig::default();
2123 let result = check_program(&program, &config);
2124 let has_await_warn = result
2125 .warnings
2126 .iter()
2127 .any(|w| w.message.contains("await") && w.message.contains("async"));
2128 assert!(
2129 !has_await_warn,
2130 "Should not warn about await inside async fn, but got: {:?}",
2131 result.warnings
2132 );
2133 }
2134}