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