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