1use std::collections::BTreeMap;
2
3use crate::ast::*;
4use crate::builtin_signatures;
5use harn_lexer::Span;
6
7#[derive(Debug, Clone)]
9pub struct TypeDiagnostic {
10 pub message: String,
11 pub severity: DiagnosticSeverity,
12 pub span: Option<Span>,
13 pub help: Option<String>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DiagnosticSeverity {
18 Error,
19 Warning,
20}
21
22type InferredType = Option<TypeExpr>;
24
25#[derive(Debug, Clone)]
27struct TypeScope {
28 vars: BTreeMap<String, InferredType>,
30 functions: BTreeMap<String, FnSignature>,
32 type_aliases: BTreeMap<String, TypeExpr>,
34 enums: BTreeMap<String, Vec<String>>,
36 interfaces: BTreeMap<String, Vec<InterfaceMethod>>,
38 structs: BTreeMap<String, Vec<(String, InferredType)>>,
40 impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
42 generic_type_params: std::collections::BTreeSet<String>,
44 where_constraints: BTreeMap<String, String>,
47 parent: Option<Box<TypeScope>>,
48}
49
50#[derive(Debug, Clone)]
52struct ImplMethodSig {
53 name: String,
54 param_count: usize,
56 param_types: Vec<Option<TypeExpr>>,
58 return_type: Option<TypeExpr>,
60}
61
62#[derive(Debug, Clone)]
63struct FnSignature {
64 params: Vec<(String, InferredType)>,
65 return_type: InferredType,
66 type_param_names: Vec<String>,
68 required_params: usize,
70 where_clauses: Vec<(String, String)>,
72}
73
74impl TypeScope {
75 fn new() -> Self {
76 Self {
77 vars: BTreeMap::new(),
78 functions: BTreeMap::new(),
79 type_aliases: BTreeMap::new(),
80 enums: BTreeMap::new(),
81 interfaces: BTreeMap::new(),
82 structs: BTreeMap::new(),
83 impl_methods: BTreeMap::new(),
84 generic_type_params: std::collections::BTreeSet::new(),
85 where_constraints: BTreeMap::new(),
86 parent: None,
87 }
88 }
89
90 fn child(&self) -> Self {
91 Self {
92 vars: BTreeMap::new(),
93 functions: BTreeMap::new(),
94 type_aliases: BTreeMap::new(),
95 enums: BTreeMap::new(),
96 interfaces: BTreeMap::new(),
97 structs: BTreeMap::new(),
98 impl_methods: BTreeMap::new(),
99 generic_type_params: std::collections::BTreeSet::new(),
100 where_constraints: BTreeMap::new(),
101 parent: Some(Box::new(self.clone())),
102 }
103 }
104
105 fn get_var(&self, name: &str) -> Option<&InferredType> {
106 self.vars
107 .get(name)
108 .or_else(|| self.parent.as_ref()?.get_var(name))
109 }
110
111 fn get_fn(&self, name: &str) -> Option<&FnSignature> {
112 self.functions
113 .get(name)
114 .or_else(|| self.parent.as_ref()?.get_fn(name))
115 }
116
117 fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
118 self.type_aliases
119 .get(name)
120 .or_else(|| self.parent.as_ref()?.resolve_type(name))
121 }
122
123 fn is_generic_type_param(&self, name: &str) -> bool {
124 self.generic_type_params.contains(name)
125 || self
126 .parent
127 .as_ref()
128 .is_some_and(|p| p.is_generic_type_param(name))
129 }
130
131 fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
132 self.where_constraints
133 .get(type_param)
134 .map(|s| s.as_str())
135 .or_else(|| {
136 self.parent
137 .as_ref()
138 .and_then(|p| p.get_where_constraint(type_param))
139 })
140 }
141
142 fn get_enum(&self, name: &str) -> Option<&Vec<String>> {
143 self.enums
144 .get(name)
145 .or_else(|| self.parent.as_ref()?.get_enum(name))
146 }
147
148 fn get_interface(&self, name: &str) -> Option<&Vec<InterfaceMethod>> {
149 self.interfaces
150 .get(name)
151 .or_else(|| self.parent.as_ref()?.get_interface(name))
152 }
153
154 fn get_struct(&self, name: &str) -> Option<&Vec<(String, InferredType)>> {
155 self.structs
156 .get(name)
157 .or_else(|| self.parent.as_ref()?.get_struct(name))
158 }
159
160 fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
161 self.impl_methods
162 .get(name)
163 .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
164 }
165
166 fn define_var(&mut self, name: &str, ty: InferredType) {
167 self.vars.insert(name.to_string(), ty);
168 }
169
170 fn define_fn(&mut self, name: &str, sig: FnSignature) {
171 self.functions.insert(name.to_string(), sig);
172 }
173}
174
175fn builtin_return_type(name: &str) -> InferredType {
178 builtin_signatures::builtin_return_type(name)
179}
180
181fn is_builtin(name: &str) -> bool {
184 builtin_signatures::is_builtin(name)
185}
186
187pub struct TypeChecker {
189 diagnostics: Vec<TypeDiagnostic>,
190 scope: TypeScope,
191}
192
193impl TypeChecker {
194 pub fn new() -> Self {
195 Self {
196 diagnostics: Vec::new(),
197 scope: TypeScope::new(),
198 }
199 }
200
201 pub fn check(mut self, program: &[SNode]) -> Vec<TypeDiagnostic> {
203 Self::register_declarations_into(&mut self.scope, program);
205
206 for snode in program {
208 if let Node::Pipeline { body, .. } = &snode.node {
209 Self::register_declarations_into(&mut self.scope, body);
210 }
211 }
212
213 for snode in program {
215 match &snode.node {
216 Node::Pipeline { params, body, .. } => {
217 let mut child = self.scope.child();
218 for p in params {
219 child.define_var(p, None);
220 }
221 self.check_block(body, &mut child);
222 }
223 Node::FnDecl {
224 name,
225 type_params,
226 params,
227 return_type,
228 where_clauses,
229 body,
230 ..
231 } => {
232 let required_params =
233 params.iter().filter(|p| p.default_value.is_none()).count();
234 let sig = FnSignature {
235 params: params
236 .iter()
237 .map(|p| (p.name.clone(), p.type_expr.clone()))
238 .collect(),
239 return_type: return_type.clone(),
240 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
241 required_params,
242 where_clauses: where_clauses
243 .iter()
244 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
245 .collect(),
246 };
247 self.scope.define_fn(name, sig);
248 self.check_fn_body(type_params, params, return_type, body, where_clauses);
249 }
250 _ => {
251 let mut scope = self.scope.clone();
252 self.check_node(snode, &mut scope);
253 for (name, ty) in scope.vars {
255 self.scope.vars.entry(name).or_insert(ty);
256 }
257 }
258 }
259 }
260
261 self.diagnostics
262 }
263
264 fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
266 for snode in nodes {
267 match &snode.node {
268 Node::TypeDecl { name, type_expr } => {
269 scope.type_aliases.insert(name.clone(), type_expr.clone());
270 }
271 Node::EnumDecl { name, variants, .. } => {
272 let variant_names: Vec<String> =
273 variants.iter().map(|v| v.name.clone()).collect();
274 scope.enums.insert(name.clone(), variant_names);
275 }
276 Node::InterfaceDecl { name, methods, .. } => {
277 scope.interfaces.insert(name.clone(), methods.clone());
278 }
279 Node::StructDecl { name, fields, .. } => {
280 let field_types: Vec<(String, InferredType)> = fields
281 .iter()
282 .map(|f| (f.name.clone(), f.type_expr.clone()))
283 .collect();
284 scope.structs.insert(name.clone(), field_types);
285 }
286 Node::ImplBlock {
287 type_name, methods, ..
288 } => {
289 let sigs: Vec<ImplMethodSig> = methods
290 .iter()
291 .filter_map(|m| {
292 if let Node::FnDecl {
293 name,
294 params,
295 return_type,
296 ..
297 } = &m.node
298 {
299 let non_self: Vec<_> =
300 params.iter().filter(|p| p.name != "self").collect();
301 let param_count = non_self.len();
302 let param_types: Vec<Option<TypeExpr>> =
303 non_self.iter().map(|p| p.type_expr.clone()).collect();
304 Some(ImplMethodSig {
305 name: name.clone(),
306 param_count,
307 param_types,
308 return_type: return_type.clone(),
309 })
310 } else {
311 None
312 }
313 })
314 .collect();
315 scope.impl_methods.insert(type_name.clone(), sigs);
316 }
317 _ => {}
318 }
319 }
320 }
321
322 fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
323 for stmt in stmts {
324 self.check_node(stmt, scope);
325 }
326 }
327
328 fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope) {
330 match pattern {
331 BindingPattern::Identifier(name) => {
332 scope.define_var(name, None);
333 }
334 BindingPattern::Dict(fields) => {
335 for field in fields {
336 let name = field.alias.as_deref().unwrap_or(&field.key);
337 scope.define_var(name, None);
338 }
339 }
340 BindingPattern::List(elements) => {
341 for elem in elements {
342 scope.define_var(&elem.name, None);
343 }
344 }
345 }
346 }
347
348 fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
349 let span = snode.span;
350 match &snode.node {
351 Node::LetBinding {
352 pattern,
353 type_ann,
354 value,
355 } => {
356 let inferred = self.infer_type(value, scope);
357 if let BindingPattern::Identifier(name) = pattern {
358 if let Some(expected) = type_ann {
359 if let Some(actual) = &inferred {
360 if !self.types_compatible(expected, actual, scope) {
361 let mut msg = format!(
362 "Type mismatch: '{}' declared as {}, but assigned {}",
363 name,
364 format_type(expected),
365 format_type(actual)
366 );
367 if let Some(detail) = shape_mismatch_detail(expected, actual) {
368 msg.push_str(&format!(" ({})", detail));
369 }
370 self.error_at(msg, span);
371 }
372 }
373 }
374 let ty = type_ann.clone().or(inferred);
375 scope.define_var(name, ty);
376 } else {
377 Self::define_pattern_vars(pattern, scope);
378 }
379 }
380
381 Node::VarBinding {
382 pattern,
383 type_ann,
384 value,
385 } => {
386 let inferred = self.infer_type(value, scope);
387 if let BindingPattern::Identifier(name) = pattern {
388 if let Some(expected) = type_ann {
389 if let Some(actual) = &inferred {
390 if !self.types_compatible(expected, actual, scope) {
391 let mut msg = format!(
392 "Type mismatch: '{}' declared as {}, but assigned {}",
393 name,
394 format_type(expected),
395 format_type(actual)
396 );
397 if let Some(detail) = shape_mismatch_detail(expected, actual) {
398 msg.push_str(&format!(" ({})", detail));
399 }
400 self.error_at(msg, span);
401 }
402 }
403 }
404 let ty = type_ann.clone().or(inferred);
405 scope.define_var(name, ty);
406 } else {
407 Self::define_pattern_vars(pattern, scope);
408 }
409 }
410
411 Node::FnDecl {
412 name,
413 type_params,
414 params,
415 return_type,
416 where_clauses,
417 body,
418 ..
419 } => {
420 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
421 let sig = FnSignature {
422 params: params
423 .iter()
424 .map(|p| (p.name.clone(), p.type_expr.clone()))
425 .collect(),
426 return_type: return_type.clone(),
427 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
428 required_params,
429 where_clauses: where_clauses
430 .iter()
431 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
432 .collect(),
433 };
434 scope.define_fn(name, sig.clone());
435 scope.define_var(name, None);
436 self.check_fn_body(type_params, params, return_type, body, where_clauses);
437 }
438
439 Node::ToolDecl {
440 name,
441 params,
442 return_type,
443 body,
444 ..
445 } => {
446 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
448 let sig = FnSignature {
449 params: params
450 .iter()
451 .map(|p| (p.name.clone(), p.type_expr.clone()))
452 .collect(),
453 return_type: return_type.clone(),
454 type_param_names: Vec::new(),
455 required_params,
456 where_clauses: Vec::new(),
457 };
458 scope.define_fn(name, sig);
459 scope.define_var(name, None);
460 self.check_fn_body(&[], params, return_type, body, &[]);
461 }
462
463 Node::FunctionCall { name, args } => {
464 self.check_call(name, args, scope, span);
465 }
466
467 Node::IfElse {
468 condition,
469 then_body,
470 else_body,
471 } => {
472 self.check_node(condition, scope);
473 let mut then_scope = scope.child();
474 if let Some((var_name, narrowed)) = Self::extract_nil_narrowing(condition, scope) {
476 then_scope.define_var(&var_name, narrowed);
477 }
478 self.check_block(then_body, &mut then_scope);
479 if let Some(else_body) = else_body {
480 let mut else_scope = scope.child();
481 self.check_block(else_body, &mut else_scope);
482 }
483 }
484
485 Node::ForIn {
486 pattern,
487 iterable,
488 body,
489 } => {
490 self.check_node(iterable, scope);
491 let mut loop_scope = scope.child();
492 if let BindingPattern::Identifier(variable) = pattern {
493 let elem_type = match self.infer_type(iterable, scope) {
495 Some(TypeExpr::List(inner)) => Some(*inner),
496 Some(TypeExpr::Named(n)) if n == "string" => {
497 Some(TypeExpr::Named("string".into()))
498 }
499 _ => None,
500 };
501 loop_scope.define_var(variable, elem_type);
502 } else {
503 Self::define_pattern_vars(pattern, &mut loop_scope);
504 }
505 self.check_block(body, &mut loop_scope);
506 }
507
508 Node::WhileLoop { condition, body } => {
509 self.check_node(condition, scope);
510 let mut loop_scope = scope.child();
511 self.check_block(body, &mut loop_scope);
512 }
513
514 Node::RequireStmt { condition, message } => {
515 self.check_node(condition, scope);
516 if let Some(message) = message {
517 self.check_node(message, scope);
518 }
519 }
520
521 Node::TryCatch {
522 body,
523 error_var,
524 catch_body,
525 finally_body,
526 ..
527 } => {
528 let mut try_scope = scope.child();
529 self.check_block(body, &mut try_scope);
530 let mut catch_scope = scope.child();
531 if let Some(var) = error_var {
532 catch_scope.define_var(var, None);
533 }
534 self.check_block(catch_body, &mut catch_scope);
535 if let Some(fb) = finally_body {
536 let mut finally_scope = scope.child();
537 self.check_block(fb, &mut finally_scope);
538 }
539 }
540
541 Node::TryExpr { body } => {
542 let mut try_scope = scope.child();
543 self.check_block(body, &mut try_scope);
544 }
545
546 Node::ReturnStmt {
547 value: Some(val), ..
548 } => {
549 self.check_node(val, scope);
550 }
551
552 Node::Assignment {
553 target, value, op, ..
554 } => {
555 self.check_node(value, scope);
556 if let Node::Identifier(name) = &target.node {
557 if let Some(Some(var_type)) = scope.get_var(name) {
558 let value_type = self.infer_type(value, scope);
559 let assigned = if let Some(op) = op {
560 let var_inferred = scope.get_var(name).cloned().flatten();
561 infer_binary_op_type(op, &var_inferred, &value_type)
562 } else {
563 value_type
564 };
565 if let Some(actual) = &assigned {
566 if !self.types_compatible(var_type, actual, scope) {
567 self.error_at(
568 format!(
569 "Type mismatch: cannot assign {} to '{}' (declared as {})",
570 format_type(actual),
571 name,
572 format_type(var_type)
573 ),
574 span,
575 );
576 }
577 }
578 }
579 }
580 }
581
582 Node::TypeDecl { name, type_expr } => {
583 scope.type_aliases.insert(name.clone(), type_expr.clone());
584 }
585
586 Node::EnumDecl { name, variants, .. } => {
587 let variant_names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
588 scope.enums.insert(name.clone(), variant_names);
589 }
590
591 Node::StructDecl { name, fields, .. } => {
592 let field_types: Vec<(String, InferredType)> = fields
593 .iter()
594 .map(|f| (f.name.clone(), f.type_expr.clone()))
595 .collect();
596 scope.structs.insert(name.clone(), field_types);
597 }
598
599 Node::InterfaceDecl { name, methods, .. } => {
600 scope.interfaces.insert(name.clone(), methods.clone());
601 }
602
603 Node::ImplBlock {
604 type_name, methods, ..
605 } => {
606 let sigs: Vec<ImplMethodSig> = methods
608 .iter()
609 .filter_map(|m| {
610 if let Node::FnDecl {
611 name,
612 params,
613 return_type,
614 ..
615 } = &m.node
616 {
617 let non_self: Vec<_> =
618 params.iter().filter(|p| p.name != "self").collect();
619 let param_count = non_self.len();
620 let param_types: Vec<Option<TypeExpr>> =
621 non_self.iter().map(|p| p.type_expr.clone()).collect();
622 Some(ImplMethodSig {
623 name: name.clone(),
624 param_count,
625 param_types,
626 return_type: return_type.clone(),
627 })
628 } else {
629 None
630 }
631 })
632 .collect();
633 scope.impl_methods.insert(type_name.clone(), sigs);
634 for method_sn in methods {
635 self.check_node(method_sn, scope);
636 }
637 }
638
639 Node::TryOperator { operand } => {
640 self.check_node(operand, scope);
641 }
642
643 Node::MatchExpr { value, arms } => {
644 self.check_node(value, scope);
645 let value_type = self.infer_type(value, scope);
646 for arm in arms {
647 self.check_node(&arm.pattern, scope);
648 if let Some(ref vt) = value_type {
650 let value_type_name = format_type(vt);
651 let mismatch = match &arm.pattern.node {
652 Node::StringLiteral(_) => {
653 !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
654 }
655 Node::IntLiteral(_) => {
656 !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
657 && !self.types_compatible(
658 vt,
659 &TypeExpr::Named("float".into()),
660 scope,
661 )
662 }
663 Node::FloatLiteral(_) => {
664 !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
665 && !self.types_compatible(
666 vt,
667 &TypeExpr::Named("int".into()),
668 scope,
669 )
670 }
671 Node::BoolLiteral(_) => {
672 !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
673 }
674 _ => false,
675 };
676 if mismatch {
677 let pattern_type = match &arm.pattern.node {
678 Node::StringLiteral(_) => "string",
679 Node::IntLiteral(_) => "int",
680 Node::FloatLiteral(_) => "float",
681 Node::BoolLiteral(_) => "bool",
682 _ => unreachable!(),
683 };
684 self.warning_at(
685 format!(
686 "Match pattern type mismatch: matching {} against {} literal",
687 value_type_name, pattern_type
688 ),
689 arm.pattern.span,
690 );
691 }
692 }
693 let mut arm_scope = scope.child();
694 self.check_block(&arm.body, &mut arm_scope);
695 }
696 self.check_match_exhaustiveness(value, arms, scope, span);
697 }
698
699 Node::BinaryOp { op, left, right } => {
701 self.check_node(left, scope);
702 self.check_node(right, scope);
703 let lt = self.infer_type(left, scope);
705 let rt = self.infer_type(right, scope);
706 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
707 match op.as_str() {
708 "-" | "*" | "/" | "%" => {
709 let numeric = ["int", "float"];
710 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
711 self.warning_at(
712 format!(
713 "Operator '{op}' may not be valid for types {} and {}",
714 l, r
715 ),
716 span,
717 );
718 }
719 }
720 "+" => {
721 let valid = ["int", "float", "string", "list", "dict"];
723 if !valid.contains(&l.as_str()) && !valid.contains(&r.as_str()) {
724 self.warning_at(
725 format!(
726 "Operator '+' may not be valid for types {} and {}",
727 l, r
728 ),
729 span,
730 );
731 }
732 }
733 _ => {}
734 }
735 }
736 }
737 Node::UnaryOp { operand, .. } => {
738 self.check_node(operand, scope);
739 }
740 Node::MethodCall {
741 object,
742 method,
743 args,
744 ..
745 }
746 | Node::OptionalMethodCall {
747 object,
748 method,
749 args,
750 ..
751 } => {
752 self.check_node(object, scope);
753 for arg in args {
754 self.check_node(arg, scope);
755 }
756 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
760 if scope.is_generic_type_param(&type_name) {
761 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
762 if let Some(iface_methods) = scope.get_interface(iface_name) {
763 let has_method = iface_methods.iter().any(|m| m.name == *method);
764 if !has_method {
765 self.warning_at(
766 format!(
767 "Method '{}' not found in interface '{}' (constraint on '{}')",
768 method, iface_name, type_name
769 ),
770 span,
771 );
772 }
773 }
774 }
775 }
776 }
777 }
778 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
779 self.check_node(object, scope);
780 }
781 Node::SubscriptAccess { object, index } => {
782 self.check_node(object, scope);
783 self.check_node(index, scope);
784 }
785 Node::SliceAccess { object, start, end } => {
786 self.check_node(object, scope);
787 if let Some(s) = start {
788 self.check_node(s, scope);
789 }
790 if let Some(e) = end {
791 self.check_node(e, scope);
792 }
793 }
794
795 Node::Ternary {
797 condition,
798 true_expr,
799 false_expr,
800 } => {
801 self.check_node(condition, scope);
802 self.check_node(true_expr, scope);
803 self.check_node(false_expr, scope);
804 }
805
806 Node::ThrowStmt { value } => {
807 self.check_node(value, scope);
808 }
809
810 Node::GuardStmt {
811 condition,
812 else_body,
813 } => {
814 self.check_node(condition, scope);
815 let mut else_scope = scope.child();
816 self.check_block(else_body, &mut else_scope);
817 }
818
819 Node::SpawnExpr { body } => {
820 let mut spawn_scope = scope.child();
821 self.check_block(body, &mut spawn_scope);
822 }
823
824 Node::Parallel {
825 count,
826 variable,
827 body,
828 } => {
829 self.check_node(count, scope);
830 let mut par_scope = scope.child();
831 if let Some(var) = variable {
832 par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
833 }
834 self.check_block(body, &mut par_scope);
835 }
836
837 Node::ParallelMap {
838 list,
839 variable,
840 body,
841 }
842 | Node::ParallelSettle {
843 list,
844 variable,
845 body,
846 } => {
847 self.check_node(list, scope);
848 let mut par_scope = scope.child();
849 let elem_type = match self.infer_type(list, scope) {
850 Some(TypeExpr::List(inner)) => Some(*inner),
851 _ => None,
852 };
853 par_scope.define_var(variable, elem_type);
854 self.check_block(body, &mut par_scope);
855 }
856
857 Node::SelectExpr {
858 cases,
859 timeout,
860 default_body,
861 } => {
862 for case in cases {
863 self.check_node(&case.channel, scope);
864 let mut case_scope = scope.child();
865 case_scope.define_var(&case.variable, None);
866 self.check_block(&case.body, &mut case_scope);
867 }
868 if let Some((dur, body)) = timeout {
869 self.check_node(dur, scope);
870 let mut timeout_scope = scope.child();
871 self.check_block(body, &mut timeout_scope);
872 }
873 if let Some(body) = default_body {
874 let mut default_scope = scope.child();
875 self.check_block(body, &mut default_scope);
876 }
877 }
878
879 Node::DeadlineBlock { duration, body } => {
880 self.check_node(duration, scope);
881 let mut block_scope = scope.child();
882 self.check_block(body, &mut block_scope);
883 }
884
885 Node::MutexBlock { body } => {
886 let mut block_scope = scope.child();
887 self.check_block(body, &mut block_scope);
888 }
889
890 Node::Retry { count, body } => {
891 self.check_node(count, scope);
892 let mut retry_scope = scope.child();
893 self.check_block(body, &mut retry_scope);
894 }
895
896 Node::Closure { params, body, .. } => {
897 let mut closure_scope = scope.child();
898 for p in params {
899 closure_scope.define_var(&p.name, p.type_expr.clone());
900 }
901 self.check_block(body, &mut closure_scope);
902 }
903
904 Node::ListLiteral(elements) => {
905 for elem in elements {
906 self.check_node(elem, scope);
907 }
908 }
909
910 Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
911 for entry in entries {
912 self.check_node(&entry.key, scope);
913 self.check_node(&entry.value, scope);
914 }
915 }
916
917 Node::RangeExpr { start, end, .. } => {
918 self.check_node(start, scope);
919 self.check_node(end, scope);
920 }
921
922 Node::Spread(inner) => {
923 self.check_node(inner, scope);
924 }
925
926 Node::Block(stmts) => {
927 let mut block_scope = scope.child();
928 self.check_block(stmts, &mut block_scope);
929 }
930
931 Node::YieldExpr { value } => {
932 if let Some(v) = value {
933 self.check_node(v, scope);
934 }
935 }
936
937 Node::StructConstruct {
939 struct_name,
940 fields,
941 } => {
942 for entry in fields {
943 self.check_node(&entry.key, scope);
944 self.check_node(&entry.value, scope);
945 }
946 if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
947 for entry in fields {
949 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
950 if !declared_fields.iter().any(|(name, _)| name == key) {
951 self.warning_at(
952 format!("Unknown field '{}' in struct '{}'", key, struct_name),
953 entry.key.span,
954 );
955 }
956 }
957 }
958 let provided: Vec<String> = fields
960 .iter()
961 .filter_map(|e| match &e.key.node {
962 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
963 _ => None,
964 })
965 .collect();
966 for (name, _) in &declared_fields {
967 if !provided.contains(name) {
968 self.warning_at(
969 format!(
970 "Missing field '{}' in struct '{}' construction",
971 name, struct_name
972 ),
973 span,
974 );
975 }
976 }
977 }
978 }
979
980 Node::EnumConstruct {
982 enum_name,
983 variant,
984 args,
985 } => {
986 for arg in args {
987 self.check_node(arg, scope);
988 }
989 if let Some(variants) = scope.get_enum(enum_name) {
990 if !variants.contains(variant) {
991 self.warning_at(
992 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
993 span,
994 );
995 }
996 }
997 }
998
999 Node::InterpolatedString(_) => {}
1001
1002 Node::StringLiteral(_)
1004 | Node::IntLiteral(_)
1005 | Node::FloatLiteral(_)
1006 | Node::BoolLiteral(_)
1007 | Node::NilLiteral
1008 | Node::Identifier(_)
1009 | Node::DurationLiteral(_)
1010 | Node::BreakStmt
1011 | Node::ContinueStmt
1012 | Node::ReturnStmt { value: None }
1013 | Node::ImportDecl { .. }
1014 | Node::SelectiveImport { .. } => {}
1015
1016 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1019 let mut decl_scope = scope.child();
1020 self.check_block(body, &mut decl_scope);
1021 }
1022 }
1023 }
1024
1025 fn check_fn_body(
1026 &mut self,
1027 type_params: &[TypeParam],
1028 params: &[TypedParam],
1029 return_type: &Option<TypeExpr>,
1030 body: &[SNode],
1031 where_clauses: &[WhereClause],
1032 ) {
1033 let mut fn_scope = self.scope.child();
1034 for tp in type_params {
1037 fn_scope.generic_type_params.insert(tp.name.clone());
1038 }
1039 for wc in where_clauses {
1041 fn_scope
1042 .where_constraints
1043 .insert(wc.type_name.clone(), wc.bound.clone());
1044 }
1045 for param in params {
1046 fn_scope.define_var(¶m.name, param.type_expr.clone());
1047 if let Some(default) = ¶m.default_value {
1048 self.check_node(default, &mut fn_scope);
1049 }
1050 }
1051 self.check_block(body, &mut fn_scope);
1052
1053 if let Some(ret_type) = return_type {
1055 for stmt in body {
1056 self.check_return_type(stmt, ret_type, &fn_scope);
1057 }
1058 }
1059 }
1060
1061 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &TypeScope) {
1062 let span = snode.span;
1063 match &snode.node {
1064 Node::ReturnStmt { value: Some(val) } => {
1065 let inferred = self.infer_type(val, scope);
1066 if let Some(actual) = &inferred {
1067 if !self.types_compatible(expected, actual, scope) {
1068 self.error_at(
1069 format!(
1070 "Return type mismatch: expected {}, got {}",
1071 format_type(expected),
1072 format_type(actual)
1073 ),
1074 span,
1075 );
1076 }
1077 }
1078 }
1079 Node::IfElse {
1080 then_body,
1081 else_body,
1082 ..
1083 } => {
1084 for stmt in then_body {
1085 self.check_return_type(stmt, expected, scope);
1086 }
1087 if let Some(else_body) = else_body {
1088 for stmt in else_body {
1089 self.check_return_type(stmt, expected, scope);
1090 }
1091 }
1092 }
1093 _ => {}
1094 }
1095 }
1096
1097 fn satisfies_interface(
1103 &self,
1104 type_name: &str,
1105 interface_name: &str,
1106 scope: &TypeScope,
1107 ) -> bool {
1108 self.interface_mismatch_reason(type_name, interface_name, scope)
1109 .is_none()
1110 }
1111
1112 fn interface_mismatch_reason(
1115 &self,
1116 type_name: &str,
1117 interface_name: &str,
1118 scope: &TypeScope,
1119 ) -> Option<String> {
1120 let interface_methods = match scope.get_interface(interface_name) {
1121 Some(methods) => methods,
1122 None => return Some(format!("interface '{}' not found", interface_name)),
1123 };
1124 let impl_methods = match scope.get_impl_methods(type_name) {
1125 Some(methods) => methods,
1126 None => {
1127 if interface_methods.is_empty() {
1128 return None;
1129 }
1130 let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1131 return Some(format!("missing method(s): {}", names.join(", ")));
1132 }
1133 };
1134 for iface_method in interface_methods {
1135 let iface_params: Vec<_> = iface_method
1136 .params
1137 .iter()
1138 .filter(|p| p.name != "self")
1139 .collect();
1140 let iface_param_count = iface_params.len();
1141 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1142 let impl_method = match matching_impl {
1143 Some(m) => m,
1144 None => {
1145 return Some(format!("missing method '{}'", iface_method.name));
1146 }
1147 };
1148 if impl_method.param_count != iface_param_count {
1149 return Some(format!(
1150 "method '{}' has {} parameter(s), expected {}",
1151 iface_method.name, impl_method.param_count, iface_param_count
1152 ));
1153 }
1154 for (i, iface_param) in iface_params.iter().enumerate() {
1156 if let (Some(expected), Some(actual)) = (
1157 &iface_param.type_expr,
1158 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1159 ) {
1160 if !self.types_compatible(expected, actual, scope) {
1161 return Some(format!(
1162 "method '{}' parameter {} has type '{}', expected '{}'",
1163 iface_method.name,
1164 i + 1,
1165 format_type(actual),
1166 format_type(expected),
1167 ));
1168 }
1169 }
1170 }
1171 if let (Some(expected_ret), Some(actual_ret)) =
1173 (&iface_method.return_type, &impl_method.return_type)
1174 {
1175 if !self.types_compatible(expected_ret, actual_ret, scope) {
1176 return Some(format!(
1177 "method '{}' returns '{}', expected '{}'",
1178 iface_method.name,
1179 format_type(actual_ret),
1180 format_type(expected_ret),
1181 ));
1182 }
1183 }
1184 }
1185 None
1186 }
1187
1188 fn extract_type_bindings(
1191 param_type: &TypeExpr,
1192 arg_type: &TypeExpr,
1193 type_params: &std::collections::BTreeSet<String>,
1194 bindings: &mut BTreeMap<String, String>,
1195 ) {
1196 match (param_type, arg_type) {
1197 (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1199 if type_params.contains(param_name) =>
1200 {
1201 bindings
1202 .entry(param_name.clone())
1203 .or_insert(concrete.clone());
1204 }
1205 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1207 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1208 }
1209 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1211 Self::extract_type_bindings(pk, ak, type_params, bindings);
1212 Self::extract_type_bindings(pv, av, type_params, bindings);
1213 }
1214 _ => {}
1215 }
1216 }
1217
1218 fn extract_nil_narrowing(
1219 condition: &SNode,
1220 scope: &TypeScope,
1221 ) -> Option<(String, InferredType)> {
1222 if let Node::BinaryOp { op, left, right } = &condition.node {
1223 if op == "!=" {
1224 let (var_node, nil_node) = if matches!(right.node, Node::NilLiteral) {
1226 (left, right)
1227 } else if matches!(left.node, Node::NilLiteral) {
1228 (right, left)
1229 } else {
1230 return None;
1231 };
1232 let _ = nil_node;
1233 if let Node::Identifier(name) = &var_node.node {
1234 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1236 let narrowed: Vec<TypeExpr> = members
1237 .iter()
1238 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
1239 .cloned()
1240 .collect();
1241 return if narrowed.len() == 1 {
1242 Some((name.clone(), Some(narrowed.into_iter().next().unwrap())))
1243 } else if narrowed.is_empty() {
1244 None
1245 } else {
1246 Some((name.clone(), Some(TypeExpr::Union(narrowed))))
1247 };
1248 }
1249 }
1250 }
1251 }
1252 None
1253 }
1254
1255 fn check_match_exhaustiveness(
1256 &mut self,
1257 value: &SNode,
1258 arms: &[MatchArm],
1259 scope: &TypeScope,
1260 span: Span,
1261 ) {
1262 let enum_name = match &value.node {
1264 Node::PropertyAccess { object, property } if property == "variant" => {
1265 match self.infer_type(object, scope) {
1267 Some(TypeExpr::Named(name)) => {
1268 if scope.get_enum(&name).is_some() {
1269 Some(name)
1270 } else {
1271 None
1272 }
1273 }
1274 _ => None,
1275 }
1276 }
1277 _ => {
1278 match self.infer_type(value, scope) {
1280 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1281 _ => None,
1282 }
1283 }
1284 };
1285
1286 let Some(enum_name) = enum_name else {
1287 return;
1288 };
1289 let Some(variants) = scope.get_enum(&enum_name) else {
1290 return;
1291 };
1292
1293 let mut covered: Vec<String> = Vec::new();
1295 let mut has_wildcard = false;
1296
1297 for arm in arms {
1298 match &arm.pattern.node {
1299 Node::StringLiteral(s) => covered.push(s.clone()),
1301 Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1303 has_wildcard = true;
1304 }
1305 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1307 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1309 _ => {
1310 has_wildcard = true;
1312 }
1313 }
1314 }
1315
1316 if has_wildcard {
1317 return;
1318 }
1319
1320 let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1321 if !missing.is_empty() {
1322 let missing_str = missing
1323 .iter()
1324 .map(|s| format!("\"{}\"", s))
1325 .collect::<Vec<_>>()
1326 .join(", ");
1327 self.warning_at(
1328 format!(
1329 "Non-exhaustive match on enum {}: missing variants {}",
1330 enum_name, missing_str
1331 ),
1332 span,
1333 );
1334 }
1335 }
1336
1337 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1338 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1340 if let Some(sig) = scope.get_fn(name).cloned() {
1341 if !has_spread
1342 && !is_builtin(name)
1343 && (args.len() < sig.required_params || args.len() > sig.params.len())
1344 {
1345 let expected = if sig.required_params == sig.params.len() {
1346 format!("{}", sig.params.len())
1347 } else {
1348 format!("{}-{}", sig.required_params, sig.params.len())
1349 };
1350 self.warning_at(
1351 format!(
1352 "Function '{}' expects {} arguments, got {}",
1353 name,
1354 expected,
1355 args.len()
1356 ),
1357 span,
1358 );
1359 }
1360 let call_scope = if sig.type_param_names.is_empty() {
1363 scope.clone()
1364 } else {
1365 let mut s = scope.child();
1366 for tp_name in &sig.type_param_names {
1367 s.generic_type_params.insert(tp_name.clone());
1368 }
1369 s
1370 };
1371 for (i, (arg, (param_name, param_type))) in
1372 args.iter().zip(sig.params.iter()).enumerate()
1373 {
1374 if let Some(expected) = param_type {
1375 let actual = self.infer_type(arg, scope);
1376 if let Some(actual) = &actual {
1377 if !self.types_compatible(expected, actual, &call_scope) {
1378 self.error_at(
1379 format!(
1380 "Argument {} ('{}'): expected {}, got {}",
1381 i + 1,
1382 param_name,
1383 format_type(expected),
1384 format_type(actual)
1385 ),
1386 arg.span,
1387 );
1388 }
1389 }
1390 }
1391 }
1392 if !sig.where_clauses.is_empty() {
1394 let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1397 let type_param_set: std::collections::BTreeSet<String> =
1398 sig.type_param_names.iter().cloned().collect();
1399 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1400 if let Some(param_ty) = param_type {
1401 if let Some(arg_ty) = self.infer_type(arg, scope) {
1402 Self::extract_type_bindings(
1403 param_ty,
1404 &arg_ty,
1405 &type_param_set,
1406 &mut type_bindings,
1407 );
1408 }
1409 }
1410 }
1411 for (type_param, bound) in &sig.where_clauses {
1412 if let Some(concrete_type) = type_bindings.get(type_param) {
1413 if let Some(reason) =
1414 self.interface_mismatch_reason(concrete_type, bound, scope)
1415 {
1416 self.warning_at(
1417 format!(
1418 "Type '{}' does not satisfy interface '{}': {} \
1419 (required by constraint `where {}: {}`)",
1420 concrete_type, bound, reason, type_param, bound
1421 ),
1422 span,
1423 );
1424 }
1425 }
1426 }
1427 }
1428 }
1429 for arg in args {
1431 self.check_node(arg, scope);
1432 }
1433 }
1434
1435 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1437 match &snode.node {
1438 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1439 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1440 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1441 Some(TypeExpr::Named("string".into()))
1442 }
1443 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1444 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1445 Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1446 Node::DictLiteral(entries) => {
1447 let mut fields = Vec::new();
1449 let mut all_string_keys = true;
1450 for entry in entries {
1451 if let Node::StringLiteral(key) = &entry.key.node {
1452 let val_type = self
1453 .infer_type(&entry.value, scope)
1454 .unwrap_or(TypeExpr::Named("nil".into()));
1455 fields.push(ShapeField {
1456 name: key.clone(),
1457 type_expr: val_type,
1458 optional: false,
1459 });
1460 } else {
1461 all_string_keys = false;
1462 break;
1463 }
1464 }
1465 if all_string_keys && !fields.is_empty() {
1466 Some(TypeExpr::Shape(fields))
1467 } else {
1468 Some(TypeExpr::Named("dict".into()))
1469 }
1470 }
1471 Node::Closure { params, body, .. } => {
1472 let all_typed = params.iter().all(|p| p.type_expr.is_some());
1474 if all_typed && !params.is_empty() {
1475 let param_types: Vec<TypeExpr> =
1476 params.iter().filter_map(|p| p.type_expr.clone()).collect();
1477 let ret = body.last().and_then(|last| self.infer_type(last, scope));
1479 if let Some(ret_type) = ret {
1480 return Some(TypeExpr::FnType {
1481 params: param_types,
1482 return_type: Box::new(ret_type),
1483 });
1484 }
1485 }
1486 Some(TypeExpr::Named("closure".into()))
1487 }
1488
1489 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1490
1491 Node::FunctionCall { name, .. } => {
1492 if scope.get_struct(name).is_some() {
1494 return Some(TypeExpr::Named(name.clone()));
1495 }
1496 if let Some(sig) = scope.get_fn(name) {
1498 return sig.return_type.clone();
1499 }
1500 builtin_return_type(name)
1502 }
1503
1504 Node::BinaryOp { op, left, right } => {
1505 let lt = self.infer_type(left, scope);
1506 let rt = self.infer_type(right, scope);
1507 infer_binary_op_type(op, <, &rt)
1508 }
1509
1510 Node::UnaryOp { op, operand } => {
1511 let t = self.infer_type(operand, scope);
1512 match op.as_str() {
1513 "!" => Some(TypeExpr::Named("bool".into())),
1514 "-" => t, _ => None,
1516 }
1517 }
1518
1519 Node::Ternary {
1520 true_expr,
1521 false_expr,
1522 ..
1523 } => {
1524 let tt = self.infer_type(true_expr, scope);
1525 let ft = self.infer_type(false_expr, scope);
1526 match (&tt, &ft) {
1527 (Some(a), Some(b)) if a == b => tt,
1528 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1529 (Some(_), None) => tt,
1530 (None, Some(_)) => ft,
1531 (None, None) => None,
1532 }
1533 }
1534
1535 Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1536
1537 Node::PropertyAccess { object, property } => {
1538 if let Node::Identifier(name) = &object.node {
1540 if scope.get_enum(name).is_some() {
1541 return Some(TypeExpr::Named(name.clone()));
1542 }
1543 }
1544 if property == "variant" {
1546 let obj_type = self.infer_type(object, scope);
1547 if let Some(TypeExpr::Named(name)) = &obj_type {
1548 if scope.get_enum(name).is_some() {
1549 return Some(TypeExpr::Named("string".into()));
1550 }
1551 }
1552 }
1553 let obj_type = self.infer_type(object, scope);
1555 if let Some(TypeExpr::Shape(fields)) = &obj_type {
1556 if let Some(field) = fields.iter().find(|f| f.name == *property) {
1557 return Some(field.type_expr.clone());
1558 }
1559 }
1560 None
1561 }
1562
1563 Node::SubscriptAccess { object, index } => {
1564 let obj_type = self.infer_type(object, scope);
1565 match &obj_type {
1566 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1567 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1568 Some(TypeExpr::Shape(fields)) => {
1569 if let Node::StringLiteral(key) = &index.node {
1571 fields
1572 .iter()
1573 .find(|f| &f.name == key)
1574 .map(|f| f.type_expr.clone())
1575 } else {
1576 None
1577 }
1578 }
1579 Some(TypeExpr::Named(n)) if n == "list" => None,
1580 Some(TypeExpr::Named(n)) if n == "dict" => None,
1581 Some(TypeExpr::Named(n)) if n == "string" => {
1582 Some(TypeExpr::Named("string".into()))
1583 }
1584 _ => None,
1585 }
1586 }
1587 Node::SliceAccess { object, .. } => {
1588 let obj_type = self.infer_type(object, scope);
1590 match &obj_type {
1591 Some(TypeExpr::List(_)) => obj_type,
1592 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1593 Some(TypeExpr::Named(n)) if n == "string" => {
1594 Some(TypeExpr::Named("string".into()))
1595 }
1596 _ => None,
1597 }
1598 }
1599 Node::MethodCall { object, method, .. }
1600 | Node::OptionalMethodCall { object, method, .. } => {
1601 let obj_type = self.infer_type(object, scope);
1602 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
1603 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
1604 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
1605 match method.as_str() {
1606 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
1608 Some(TypeExpr::Named("bool".into()))
1609 }
1610 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
1612 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
1614 | "pad_left" | "pad_right" | "repeat" | "join" => {
1615 Some(TypeExpr::Named("string".into()))
1616 }
1617 "split" | "chars" => Some(TypeExpr::Named("list".into())),
1618 "filter" => {
1620 if is_dict {
1621 Some(TypeExpr::Named("dict".into()))
1622 } else {
1623 Some(TypeExpr::Named("list".into()))
1624 }
1625 }
1626 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
1628 "reduce" | "find" | "first" | "last" => None,
1629 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
1631 "merge" | "map_values" | "rekey" | "map_keys" => {
1632 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
1636 Some(TypeExpr::DictType(
1637 Box::new(TypeExpr::Named("string".into())),
1638 v.clone(),
1639 ))
1640 } else {
1641 Some(TypeExpr::Named("dict".into()))
1642 }
1643 }
1644 "to_string" => Some(TypeExpr::Named("string".into())),
1646 "to_int" => Some(TypeExpr::Named("int".into())),
1647 "to_float" => Some(TypeExpr::Named("float".into())),
1648 _ => None,
1649 }
1650 }
1651
1652 Node::TryOperator { operand } => {
1654 match self.infer_type(operand, scope) {
1655 Some(TypeExpr::Named(name)) if name == "Result" => None, _ => None,
1657 }
1658 }
1659
1660 _ => None,
1661 }
1662 }
1663
1664 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
1666 if let TypeExpr::Named(name) = expected {
1668 if scope.is_generic_type_param(name) {
1669 return true;
1670 }
1671 }
1672 if let TypeExpr::Named(name) = actual {
1673 if scope.is_generic_type_param(name) {
1674 return true;
1675 }
1676 }
1677 let expected = self.resolve_alias(expected, scope);
1678 let actual = self.resolve_alias(actual, scope);
1679
1680 if let TypeExpr::Named(iface_name) = &expected {
1683 if scope.get_interface(iface_name).is_some() {
1684 if let TypeExpr::Named(type_name) = &actual {
1685 return self.satisfies_interface(type_name, iface_name, scope);
1686 }
1687 return false;
1688 }
1689 }
1690
1691 match (&expected, &actual) {
1692 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
1693 (TypeExpr::Union(members), actual_type) => members
1694 .iter()
1695 .any(|m| self.types_compatible(m, actual_type, scope)),
1696 (expected_type, TypeExpr::Union(members)) => members
1697 .iter()
1698 .all(|m| self.types_compatible(expected_type, m, scope)),
1699 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
1700 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
1701 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
1702 if expected_field.optional {
1703 return true;
1704 }
1705 af.iter().any(|actual_field| {
1706 actual_field.name == expected_field.name
1707 && self.types_compatible(
1708 &expected_field.type_expr,
1709 &actual_field.type_expr,
1710 scope,
1711 )
1712 })
1713 }),
1714 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
1716 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
1717 keys_ok
1718 && af
1719 .iter()
1720 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
1721 }
1722 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
1724 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
1725 self.types_compatible(expected_inner, actual_inner, scope)
1726 }
1727 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
1728 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
1729 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
1730 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
1731 }
1732 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
1733 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
1734 (
1736 TypeExpr::FnType {
1737 params: ep,
1738 return_type: er,
1739 },
1740 TypeExpr::FnType {
1741 params: ap,
1742 return_type: ar,
1743 },
1744 ) => {
1745 ep.len() == ap.len()
1746 && ep
1747 .iter()
1748 .zip(ap.iter())
1749 .all(|(e, a)| self.types_compatible(e, a, scope))
1750 && self.types_compatible(er, ar, scope)
1751 }
1752 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
1754 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
1755 _ => false,
1756 }
1757 }
1758
1759 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
1760 if let TypeExpr::Named(name) = ty {
1761 if let Some(resolved) = scope.resolve_type(name) {
1762 return resolved.clone();
1763 }
1764 }
1765 ty.clone()
1766 }
1767
1768 fn error_at(&mut self, message: String, span: Span) {
1769 self.diagnostics.push(TypeDiagnostic {
1770 message,
1771 severity: DiagnosticSeverity::Error,
1772 span: Some(span),
1773 help: None,
1774 });
1775 }
1776
1777 #[allow(dead_code)]
1778 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
1779 self.diagnostics.push(TypeDiagnostic {
1780 message,
1781 severity: DiagnosticSeverity::Error,
1782 span: Some(span),
1783 help: Some(help),
1784 });
1785 }
1786
1787 fn warning_at(&mut self, message: String, span: Span) {
1788 self.diagnostics.push(TypeDiagnostic {
1789 message,
1790 severity: DiagnosticSeverity::Warning,
1791 span: Some(span),
1792 help: None,
1793 });
1794 }
1795
1796 #[allow(dead_code)]
1797 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
1798 self.diagnostics.push(TypeDiagnostic {
1799 message,
1800 severity: DiagnosticSeverity::Warning,
1801 span: Some(span),
1802 help: Some(help),
1803 });
1804 }
1805}
1806
1807impl Default for TypeChecker {
1808 fn default() -> Self {
1809 Self::new()
1810 }
1811}
1812
1813fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
1815 match op {
1816 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
1817 Some(TypeExpr::Named("bool".into()))
1818 }
1819 "+" => match (left, right) {
1820 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
1821 match (l.as_str(), r.as_str()) {
1822 ("int", "int") => Some(TypeExpr::Named("int".into())),
1823 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
1824 ("string", _) => Some(TypeExpr::Named("string".into())),
1825 ("list", "list") => Some(TypeExpr::Named("list".into())),
1826 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
1827 _ => Some(TypeExpr::Named("string".into())),
1828 }
1829 }
1830 _ => None,
1831 },
1832 "-" | "*" | "/" | "%" => match (left, right) {
1833 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
1834 match (l.as_str(), r.as_str()) {
1835 ("int", "int") => Some(TypeExpr::Named("int".into())),
1836 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
1837 _ => None,
1838 }
1839 }
1840 _ => None,
1841 },
1842 "??" => match (left, right) {
1843 (Some(TypeExpr::Union(members)), _) => {
1844 let non_nil: Vec<_> = members
1845 .iter()
1846 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
1847 .cloned()
1848 .collect();
1849 if non_nil.len() == 1 {
1850 Some(non_nil[0].clone())
1851 } else if non_nil.is_empty() {
1852 right.clone()
1853 } else {
1854 Some(TypeExpr::Union(non_nil))
1855 }
1856 }
1857 _ => right.clone(),
1858 },
1859 "|>" => None,
1860 _ => None,
1861 }
1862}
1863
1864pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
1869 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
1870 let mut details = Vec::new();
1871 for field in ef {
1872 if field.optional {
1873 continue;
1874 }
1875 match af.iter().find(|f| f.name == field.name) {
1876 None => details.push(format!(
1877 "missing field '{}' ({})",
1878 field.name,
1879 format_type(&field.type_expr)
1880 )),
1881 Some(actual_field) => {
1882 let e_str = format_type(&field.type_expr);
1883 let a_str = format_type(&actual_field.type_expr);
1884 if e_str != a_str {
1885 details.push(format!(
1886 "field '{}' has type {}, expected {}",
1887 field.name, a_str, e_str
1888 ));
1889 }
1890 }
1891 }
1892 }
1893 if details.is_empty() {
1894 None
1895 } else {
1896 Some(details.join("; "))
1897 }
1898 } else {
1899 None
1900 }
1901}
1902
1903pub fn format_type(ty: &TypeExpr) -> String {
1904 match ty {
1905 TypeExpr::Named(n) => n.clone(),
1906 TypeExpr::Union(types) => types
1907 .iter()
1908 .map(format_type)
1909 .collect::<Vec<_>>()
1910 .join(" | "),
1911 TypeExpr::Shape(fields) => {
1912 let inner: Vec<String> = fields
1913 .iter()
1914 .map(|f| {
1915 let opt = if f.optional { "?" } else { "" };
1916 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
1917 })
1918 .collect();
1919 format!("{{{}}}", inner.join(", "))
1920 }
1921 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
1922 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
1923 TypeExpr::FnType {
1924 params,
1925 return_type,
1926 } => {
1927 let params_str = params
1928 .iter()
1929 .map(format_type)
1930 .collect::<Vec<_>>()
1931 .join(", ");
1932 format!("fn({}) -> {}", params_str, format_type(return_type))
1933 }
1934 }
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939 use super::*;
1940 use crate::Parser;
1941 use harn_lexer::Lexer;
1942
1943 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
1944 let mut lexer = Lexer::new(source);
1945 let tokens = lexer.tokenize().unwrap();
1946 let mut parser = Parser::new(tokens);
1947 let program = parser.parse().unwrap();
1948 TypeChecker::new().check(&program)
1949 }
1950
1951 fn errors(source: &str) -> Vec<String> {
1952 check_source(source)
1953 .into_iter()
1954 .filter(|d| d.severity == DiagnosticSeverity::Error)
1955 .map(|d| d.message)
1956 .collect()
1957 }
1958
1959 #[test]
1960 fn test_no_errors_for_untyped_code() {
1961 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
1962 assert!(errs.is_empty());
1963 }
1964
1965 #[test]
1966 fn test_correct_typed_let() {
1967 let errs = errors("pipeline t(task) { let x: int = 42 }");
1968 assert!(errs.is_empty());
1969 }
1970
1971 #[test]
1972 fn test_type_mismatch_let() {
1973 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
1974 assert_eq!(errs.len(), 1);
1975 assert!(errs[0].contains("Type mismatch"));
1976 assert!(errs[0].contains("int"));
1977 assert!(errs[0].contains("string"));
1978 }
1979
1980 #[test]
1981 fn test_correct_typed_fn() {
1982 let errs = errors(
1983 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
1984 );
1985 assert!(errs.is_empty());
1986 }
1987
1988 #[test]
1989 fn test_fn_arg_type_mismatch() {
1990 let errs = errors(
1991 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
1992add("hello", 2) }"#,
1993 );
1994 assert_eq!(errs.len(), 1);
1995 assert!(errs[0].contains("Argument 1"));
1996 assert!(errs[0].contains("expected int"));
1997 }
1998
1999 #[test]
2000 fn test_return_type_mismatch() {
2001 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2002 assert_eq!(errs.len(), 1);
2003 assert!(errs[0].contains("Return type mismatch"));
2004 }
2005
2006 #[test]
2007 fn test_union_type_compatible() {
2008 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2009 assert!(errs.is_empty());
2010 }
2011
2012 #[test]
2013 fn test_union_type_mismatch() {
2014 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2015 assert_eq!(errs.len(), 1);
2016 assert!(errs[0].contains("Type mismatch"));
2017 }
2018
2019 #[test]
2020 fn test_type_inference_propagation() {
2021 let errs = errors(
2022 r#"pipeline t(task) {
2023 fn add(a: int, b: int) -> int { return a + b }
2024 let result: string = add(1, 2)
2025}"#,
2026 );
2027 assert_eq!(errs.len(), 1);
2028 assert!(errs[0].contains("Type mismatch"));
2029 assert!(errs[0].contains("string"));
2030 assert!(errs[0].contains("int"));
2031 }
2032
2033 #[test]
2034 fn test_builtin_return_type_inference() {
2035 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2036 assert_eq!(errs.len(), 1);
2037 assert!(errs[0].contains("string"));
2038 assert!(errs[0].contains("int"));
2039 }
2040
2041 #[test]
2042 fn test_workflow_and_transcript_builtins_are_known() {
2043 let errs = errors(
2044 r#"pipeline t(task) {
2045 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2046 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2047 let run: dict = workflow_execute("task", flow, [], {})
2048 let tree: dict = load_run_tree("run.json")
2049 let fixture: dict = run_record_fixture(run?.run)
2050 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
2051 let diff: dict = run_record_diff(run?.run, run?.run)
2052 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
2053 let suite_report: dict = eval_suite_run(manifest)
2054 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
2055 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
2056 let selection: dict = artifact_editor_selection("src/main.rs", "main")
2057 let verify: dict = artifact_verification_result("verify", "ok")
2058 let test_result: dict = artifact_test_result("tests", "pass")
2059 let cmd: dict = artifact_command_result("cargo test", {status: 0})
2060 let patch: dict = artifact_diff("src/main.rs", "old", "new")
2061 let git: dict = artifact_git_diff("diff --git a b")
2062 let review: dict = artifact_diff_review(patch, "review me")
2063 let decision: dict = artifact_review_decision(review, "accepted")
2064 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
2065 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
2066 let apply: dict = artifact_apply_intent(review, "apply")
2067 let transcript = transcript_reset({metadata: {source: "test"}})
2068 let visible: string = transcript_render_visible(transcript_archive(transcript))
2069 let events: list = transcript_events(transcript)
2070 let context: string = artifact_context([], {max_artifacts: 1})
2071 println(report)
2072 println(run)
2073 println(tree)
2074 println(fixture)
2075 println(suite)
2076 println(diff)
2077 println(manifest)
2078 println(suite_report)
2079 println(wf)
2080 println(snap)
2081 println(selection)
2082 println(verify)
2083 println(test_result)
2084 println(cmd)
2085 println(patch)
2086 println(git)
2087 println(review)
2088 println(decision)
2089 println(proposal)
2090 println(bundle)
2091 println(apply)
2092 println(visible)
2093 println(events)
2094 println(context)
2095}"#,
2096 );
2097 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2098 }
2099
2100 #[test]
2101 fn test_binary_op_type_inference() {
2102 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
2103 assert_eq!(errs.len(), 1);
2104 }
2105
2106 #[test]
2107 fn test_comparison_returns_bool() {
2108 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
2109 assert!(errs.is_empty());
2110 }
2111
2112 #[test]
2113 fn test_int_float_promotion() {
2114 let errs = errors("pipeline t(task) { let x: float = 42 }");
2115 assert!(errs.is_empty());
2116 }
2117
2118 #[test]
2119 fn test_untyped_code_no_errors() {
2120 let errs = errors(
2121 r#"pipeline t(task) {
2122 fn process(data) {
2123 let result = data + " processed"
2124 return result
2125 }
2126 log(process("hello"))
2127}"#,
2128 );
2129 assert!(errs.is_empty());
2130 }
2131
2132 #[test]
2133 fn test_type_alias() {
2134 let errs = errors(
2135 r#"pipeline t(task) {
2136 type Name = string
2137 let x: Name = "hello"
2138}"#,
2139 );
2140 assert!(errs.is_empty());
2141 }
2142
2143 #[test]
2144 fn test_type_alias_mismatch() {
2145 let errs = errors(
2146 r#"pipeline t(task) {
2147 type Name = string
2148 let x: Name = 42
2149}"#,
2150 );
2151 assert_eq!(errs.len(), 1);
2152 }
2153
2154 #[test]
2155 fn test_assignment_type_check() {
2156 let errs = errors(
2157 r#"pipeline t(task) {
2158 var x: int = 0
2159 x = "hello"
2160}"#,
2161 );
2162 assert_eq!(errs.len(), 1);
2163 assert!(errs[0].contains("cannot assign string"));
2164 }
2165
2166 #[test]
2167 fn test_covariance_int_to_float_in_fn() {
2168 let errs = errors(
2169 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
2170 );
2171 assert!(errs.is_empty());
2172 }
2173
2174 #[test]
2175 fn test_covariance_return_type() {
2176 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
2177 assert!(errs.is_empty());
2178 }
2179
2180 #[test]
2181 fn test_no_contravariance_float_to_int() {
2182 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
2183 assert_eq!(errs.len(), 1);
2184 }
2185
2186 fn warnings(source: &str) -> Vec<String> {
2189 check_source(source)
2190 .into_iter()
2191 .filter(|d| d.severity == DiagnosticSeverity::Warning)
2192 .map(|d| d.message)
2193 .collect()
2194 }
2195
2196 #[test]
2197 fn test_exhaustive_match_no_warning() {
2198 let warns = warnings(
2199 r#"pipeline t(task) {
2200 enum Color { Red, Green, Blue }
2201 let c = Color.Red
2202 match c.variant {
2203 "Red" -> { log("r") }
2204 "Green" -> { log("g") }
2205 "Blue" -> { log("b") }
2206 }
2207}"#,
2208 );
2209 let exhaustive_warns: Vec<_> = warns
2210 .iter()
2211 .filter(|w| w.contains("Non-exhaustive"))
2212 .collect();
2213 assert!(exhaustive_warns.is_empty());
2214 }
2215
2216 #[test]
2217 fn test_non_exhaustive_match_warning() {
2218 let warns = warnings(
2219 r#"pipeline t(task) {
2220 enum Color { Red, Green, Blue }
2221 let c = Color.Red
2222 match c.variant {
2223 "Red" -> { log("r") }
2224 "Green" -> { log("g") }
2225 }
2226}"#,
2227 );
2228 let exhaustive_warns: Vec<_> = warns
2229 .iter()
2230 .filter(|w| w.contains("Non-exhaustive"))
2231 .collect();
2232 assert_eq!(exhaustive_warns.len(), 1);
2233 assert!(exhaustive_warns[0].contains("Blue"));
2234 }
2235
2236 #[test]
2237 fn test_non_exhaustive_multiple_missing() {
2238 let warns = warnings(
2239 r#"pipeline t(task) {
2240 enum Status { Active, Inactive, Pending }
2241 let s = Status.Active
2242 match s.variant {
2243 "Active" -> { log("a") }
2244 }
2245}"#,
2246 );
2247 let exhaustive_warns: Vec<_> = warns
2248 .iter()
2249 .filter(|w| w.contains("Non-exhaustive"))
2250 .collect();
2251 assert_eq!(exhaustive_warns.len(), 1);
2252 assert!(exhaustive_warns[0].contains("Inactive"));
2253 assert!(exhaustive_warns[0].contains("Pending"));
2254 }
2255
2256 #[test]
2257 fn test_enum_construct_type_inference() {
2258 let errs = errors(
2259 r#"pipeline t(task) {
2260 enum Color { Red, Green, Blue }
2261 let c: Color = Color.Red
2262}"#,
2263 );
2264 assert!(errs.is_empty());
2265 }
2266
2267 #[test]
2270 fn test_nil_coalescing_strips_nil() {
2271 let errs = errors(
2273 r#"pipeline t(task) {
2274 let x: string | nil = nil
2275 let y: string = x ?? "default"
2276}"#,
2277 );
2278 assert!(errs.is_empty());
2279 }
2280
2281 #[test]
2282 fn test_shape_mismatch_detail_missing_field() {
2283 let errs = errors(
2284 r#"pipeline t(task) {
2285 let x: {name: string, age: int} = {name: "hello"}
2286}"#,
2287 );
2288 assert_eq!(errs.len(), 1);
2289 assert!(
2290 errs[0].contains("missing field 'age'"),
2291 "expected detail about missing field, got: {}",
2292 errs[0]
2293 );
2294 }
2295
2296 #[test]
2297 fn test_shape_mismatch_detail_wrong_type() {
2298 let errs = errors(
2299 r#"pipeline t(task) {
2300 let x: {name: string, age: int} = {name: 42, age: 10}
2301}"#,
2302 );
2303 assert_eq!(errs.len(), 1);
2304 assert!(
2305 errs[0].contains("field 'name' has type int, expected string"),
2306 "expected detail about wrong type, got: {}",
2307 errs[0]
2308 );
2309 }
2310
2311 #[test]
2314 fn test_match_pattern_string_against_int() {
2315 let warns = warnings(
2316 r#"pipeline t(task) {
2317 let x: int = 42
2318 match x {
2319 "hello" -> { log("bad") }
2320 42 -> { log("ok") }
2321 }
2322}"#,
2323 );
2324 let pattern_warns: Vec<_> = warns
2325 .iter()
2326 .filter(|w| w.contains("Match pattern type mismatch"))
2327 .collect();
2328 assert_eq!(pattern_warns.len(), 1);
2329 assert!(pattern_warns[0].contains("matching int against string literal"));
2330 }
2331
2332 #[test]
2333 fn test_match_pattern_int_against_string() {
2334 let warns = warnings(
2335 r#"pipeline t(task) {
2336 let x: string = "hello"
2337 match x {
2338 42 -> { log("bad") }
2339 "hello" -> { log("ok") }
2340 }
2341}"#,
2342 );
2343 let pattern_warns: Vec<_> = warns
2344 .iter()
2345 .filter(|w| w.contains("Match pattern type mismatch"))
2346 .collect();
2347 assert_eq!(pattern_warns.len(), 1);
2348 assert!(pattern_warns[0].contains("matching string against int literal"));
2349 }
2350
2351 #[test]
2352 fn test_match_pattern_bool_against_int() {
2353 let warns = warnings(
2354 r#"pipeline t(task) {
2355 let x: int = 42
2356 match x {
2357 true -> { log("bad") }
2358 42 -> { log("ok") }
2359 }
2360}"#,
2361 );
2362 let pattern_warns: Vec<_> = warns
2363 .iter()
2364 .filter(|w| w.contains("Match pattern type mismatch"))
2365 .collect();
2366 assert_eq!(pattern_warns.len(), 1);
2367 assert!(pattern_warns[0].contains("matching int against bool literal"));
2368 }
2369
2370 #[test]
2371 fn test_match_pattern_float_against_string() {
2372 let warns = warnings(
2373 r#"pipeline t(task) {
2374 let x: string = "hello"
2375 match x {
2376 3.14 -> { log("bad") }
2377 "hello" -> { log("ok") }
2378 }
2379}"#,
2380 );
2381 let pattern_warns: Vec<_> = warns
2382 .iter()
2383 .filter(|w| w.contains("Match pattern type mismatch"))
2384 .collect();
2385 assert_eq!(pattern_warns.len(), 1);
2386 assert!(pattern_warns[0].contains("matching string against float literal"));
2387 }
2388
2389 #[test]
2390 fn test_match_pattern_int_against_float_ok() {
2391 let warns = warnings(
2393 r#"pipeline t(task) {
2394 let x: float = 3.14
2395 match x {
2396 42 -> { log("ok") }
2397 _ -> { log("default") }
2398 }
2399}"#,
2400 );
2401 let pattern_warns: Vec<_> = warns
2402 .iter()
2403 .filter(|w| w.contains("Match pattern type mismatch"))
2404 .collect();
2405 assert!(pattern_warns.is_empty());
2406 }
2407
2408 #[test]
2409 fn test_match_pattern_float_against_int_ok() {
2410 let warns = warnings(
2412 r#"pipeline t(task) {
2413 let x: int = 42
2414 match x {
2415 3.14 -> { log("close") }
2416 _ -> { log("default") }
2417 }
2418}"#,
2419 );
2420 let pattern_warns: Vec<_> = warns
2421 .iter()
2422 .filter(|w| w.contains("Match pattern type mismatch"))
2423 .collect();
2424 assert!(pattern_warns.is_empty());
2425 }
2426
2427 #[test]
2428 fn test_match_pattern_correct_types_no_warning() {
2429 let warns = warnings(
2430 r#"pipeline t(task) {
2431 let x: int = 42
2432 match x {
2433 1 -> { log("one") }
2434 2 -> { log("two") }
2435 _ -> { log("other") }
2436 }
2437}"#,
2438 );
2439 let pattern_warns: Vec<_> = warns
2440 .iter()
2441 .filter(|w| w.contains("Match pattern type mismatch"))
2442 .collect();
2443 assert!(pattern_warns.is_empty());
2444 }
2445
2446 #[test]
2447 fn test_match_pattern_wildcard_no_warning() {
2448 let warns = warnings(
2449 r#"pipeline t(task) {
2450 let x: int = 42
2451 match x {
2452 _ -> { log("catch all") }
2453 }
2454}"#,
2455 );
2456 let pattern_warns: Vec<_> = warns
2457 .iter()
2458 .filter(|w| w.contains("Match pattern type mismatch"))
2459 .collect();
2460 assert!(pattern_warns.is_empty());
2461 }
2462
2463 #[test]
2464 fn test_match_pattern_untyped_no_warning() {
2465 let warns = warnings(
2467 r#"pipeline t(task) {
2468 let x = some_unknown_fn()
2469 match x {
2470 "hello" -> { log("string") }
2471 42 -> { log("int") }
2472 }
2473}"#,
2474 );
2475 let pattern_warns: Vec<_> = warns
2476 .iter()
2477 .filter(|w| w.contains("Match pattern type mismatch"))
2478 .collect();
2479 assert!(pattern_warns.is_empty());
2480 }
2481
2482 fn iface_warns(source: &str) -> Vec<String> {
2485 warnings(source)
2486 .into_iter()
2487 .filter(|w| w.contains("does not satisfy interface"))
2488 .collect()
2489 }
2490
2491 #[test]
2492 fn test_interface_constraint_return_type_mismatch() {
2493 let warns = iface_warns(
2494 r#"pipeline t(task) {
2495 interface Sizable {
2496 fn size(self) -> int
2497 }
2498 struct Box { width: int }
2499 impl Box {
2500 fn size(self) -> string { return "nope" }
2501 }
2502 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2503 measure(Box({width: 3}))
2504}"#,
2505 );
2506 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2507 assert!(
2508 warns[0].contains("method 'size' returns 'string', expected 'int'"),
2509 "unexpected message: {}",
2510 warns[0]
2511 );
2512 }
2513
2514 #[test]
2515 fn test_interface_constraint_param_type_mismatch() {
2516 let warns = iface_warns(
2517 r#"pipeline t(task) {
2518 interface Processor {
2519 fn process(self, x: int) -> string
2520 }
2521 struct MyProc { name: string }
2522 impl MyProc {
2523 fn process(self, x: string) -> string { return x }
2524 }
2525 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
2526 run_proc(MyProc({name: "a"}))
2527}"#,
2528 );
2529 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2530 assert!(
2531 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
2532 "unexpected message: {}",
2533 warns[0]
2534 );
2535 }
2536
2537 #[test]
2538 fn test_interface_constraint_missing_method() {
2539 let warns = iface_warns(
2540 r#"pipeline t(task) {
2541 interface Sizable {
2542 fn size(self) -> int
2543 }
2544 struct Box { width: int }
2545 impl Box {
2546 fn area(self) -> int { return self.width }
2547 }
2548 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2549 measure(Box({width: 3}))
2550}"#,
2551 );
2552 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2553 assert!(
2554 warns[0].contains("missing method 'size'"),
2555 "unexpected message: {}",
2556 warns[0]
2557 );
2558 }
2559
2560 #[test]
2561 fn test_interface_constraint_param_count_mismatch() {
2562 let warns = iface_warns(
2563 r#"pipeline t(task) {
2564 interface Doubler {
2565 fn double(self, x: int) -> int
2566 }
2567 struct Bad { v: int }
2568 impl Bad {
2569 fn double(self) -> int { return self.v * 2 }
2570 }
2571 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
2572 run_double(Bad({v: 5}))
2573}"#,
2574 );
2575 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2576 assert!(
2577 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
2578 "unexpected message: {}",
2579 warns[0]
2580 );
2581 }
2582
2583 #[test]
2584 fn test_interface_constraint_satisfied() {
2585 let warns = iface_warns(
2586 r#"pipeline t(task) {
2587 interface Sizable {
2588 fn size(self) -> int
2589 }
2590 struct Box { width: int, height: int }
2591 impl Box {
2592 fn size(self) -> int { return self.width * self.height }
2593 }
2594 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2595 measure(Box({width: 3, height: 4}))
2596}"#,
2597 );
2598 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2599 }
2600
2601 #[test]
2602 fn test_interface_constraint_untyped_impl_compatible() {
2603 let warns = iface_warns(
2605 r#"pipeline t(task) {
2606 interface Sizable {
2607 fn size(self) -> int
2608 }
2609 struct Box { width: int }
2610 impl Box {
2611 fn size(self) { return self.width }
2612 }
2613 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2614 measure(Box({width: 3}))
2615}"#,
2616 );
2617 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2618 }
2619
2620 #[test]
2621 fn test_interface_constraint_int_float_covariance() {
2622 let warns = iface_warns(
2624 r#"pipeline t(task) {
2625 interface Measurable {
2626 fn value(self) -> float
2627 }
2628 struct Gauge { v: int }
2629 impl Gauge {
2630 fn value(self) -> int { return self.v }
2631 }
2632 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
2633 read_val(Gauge({v: 42}))
2634}"#,
2635 );
2636 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2637 }
2638}