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