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 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()); 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 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(_) => {} }
865 }
866
867 StmtKind::StructDecl { name, .. } => {
868 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 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 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 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 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 if let Some(trait_info) = self.env.lookup_trait(trait_name).cloned() {
965 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 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 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 self.env
1018 .register_type_alias(name.clone(), type_params.clone(), value.clone());
1019 }
1020 }
1021 }
1022
1023 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 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 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 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 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 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 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 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 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 fn check_unused_vars(&mut self) {
1243 for (name, span, _depth) in &self.defined_vars {
1244 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 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
1276pub 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
1313pub fn check_match_exhaustiveness_patterns(
1316 subject_type: &Type,
1317 arms: &[MatchArm],
1318 env: &TypeEnv,
1319) -> Vec<String> {
1320 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 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
1337fn 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 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 #[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 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 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 #[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 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 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 #[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 #[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 #[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 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 #[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}