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