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