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