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