1use std::collections::BTreeMap;
2
3use crate::ast::*;
4use crate::builtin_signatures;
5use harn_lexer::{FixEdit, Span};
6
7#[derive(Debug, Clone)]
9pub struct TypeDiagnostic {
10 pub message: String,
11 pub severity: DiagnosticSeverity,
12 pub span: Option<Span>,
13 pub help: Option<String>,
14 pub fix: Option<Vec<FixEdit>>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum DiagnosticSeverity {
20 Error,
21 Warning,
22}
23
24type InferredType = Option<TypeExpr>;
26
27#[derive(Debug, Clone)]
29struct TypeScope {
30 vars: BTreeMap<String, InferredType>,
32 functions: BTreeMap<String, FnSignature>,
34 type_aliases: BTreeMap<String, TypeExpr>,
36 enums: BTreeMap<String, Vec<String>>,
38 interfaces: BTreeMap<String, Vec<InterfaceMethod>>,
40 structs: BTreeMap<String, Vec<(String, InferredType)>>,
42 impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
44 generic_type_params: std::collections::BTreeSet<String>,
46 where_constraints: BTreeMap<String, String>,
49 mutable_vars: std::collections::BTreeSet<String>,
52 narrowed_vars: BTreeMap<String, InferredType>,
55 parent: Option<Box<TypeScope>>,
56}
57
58#[derive(Debug, Clone)]
60struct ImplMethodSig {
61 name: String,
62 param_count: usize,
64 param_types: Vec<Option<TypeExpr>>,
66 return_type: Option<TypeExpr>,
68}
69
70#[derive(Debug, Clone)]
71struct FnSignature {
72 params: Vec<(String, InferredType)>,
73 return_type: InferredType,
74 type_param_names: Vec<String>,
76 required_params: usize,
78 where_clauses: Vec<(String, String)>,
80 has_rest: bool,
82}
83
84impl TypeScope {
85 fn new() -> Self {
86 Self {
87 vars: BTreeMap::new(),
88 functions: BTreeMap::new(),
89 type_aliases: BTreeMap::new(),
90 enums: BTreeMap::new(),
91 interfaces: BTreeMap::new(),
92 structs: BTreeMap::new(),
93 impl_methods: BTreeMap::new(),
94 generic_type_params: std::collections::BTreeSet::new(),
95 where_constraints: BTreeMap::new(),
96 mutable_vars: std::collections::BTreeSet::new(),
97 narrowed_vars: BTreeMap::new(),
98 parent: None,
99 }
100 }
101
102 fn child(&self) -> Self {
103 Self {
104 vars: BTreeMap::new(),
105 functions: BTreeMap::new(),
106 type_aliases: BTreeMap::new(),
107 enums: BTreeMap::new(),
108 interfaces: BTreeMap::new(),
109 structs: BTreeMap::new(),
110 impl_methods: BTreeMap::new(),
111 generic_type_params: std::collections::BTreeSet::new(),
112 where_constraints: BTreeMap::new(),
113 mutable_vars: std::collections::BTreeSet::new(),
114 narrowed_vars: BTreeMap::new(),
115 parent: Some(Box::new(self.clone())),
116 }
117 }
118
119 fn get_var(&self, name: &str) -> Option<&InferredType> {
120 self.vars
121 .get(name)
122 .or_else(|| self.parent.as_ref()?.get_var(name))
123 }
124
125 fn get_fn(&self, name: &str) -> Option<&FnSignature> {
126 self.functions
127 .get(name)
128 .or_else(|| self.parent.as_ref()?.get_fn(name))
129 }
130
131 fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
132 self.type_aliases
133 .get(name)
134 .or_else(|| self.parent.as_ref()?.resolve_type(name))
135 }
136
137 fn is_generic_type_param(&self, name: &str) -> bool {
138 self.generic_type_params.contains(name)
139 || self
140 .parent
141 .as_ref()
142 .is_some_and(|p| p.is_generic_type_param(name))
143 }
144
145 fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
146 self.where_constraints
147 .get(type_param)
148 .map(|s| s.as_str())
149 .or_else(|| {
150 self.parent
151 .as_ref()
152 .and_then(|p| p.get_where_constraint(type_param))
153 })
154 }
155
156 fn get_enum(&self, name: &str) -> Option<&Vec<String>> {
157 self.enums
158 .get(name)
159 .or_else(|| self.parent.as_ref()?.get_enum(name))
160 }
161
162 fn get_interface(&self, name: &str) -> Option<&Vec<InterfaceMethod>> {
163 self.interfaces
164 .get(name)
165 .or_else(|| self.parent.as_ref()?.get_interface(name))
166 }
167
168 fn get_struct(&self, name: &str) -> Option<&Vec<(String, InferredType)>> {
169 self.structs
170 .get(name)
171 .or_else(|| self.parent.as_ref()?.get_struct(name))
172 }
173
174 fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
175 self.impl_methods
176 .get(name)
177 .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
178 }
179
180 fn define_var(&mut self, name: &str, ty: InferredType) {
181 self.vars.insert(name.to_string(), ty);
182 }
183
184 fn define_var_mutable(&mut self, name: &str, ty: InferredType) {
185 self.vars.insert(name.to_string(), ty);
186 self.mutable_vars.insert(name.to_string());
187 }
188
189 fn is_mutable(&self, name: &str) -> bool {
191 self.mutable_vars.contains(name) || self.parent.as_ref().is_some_and(|p| p.is_mutable(name))
192 }
193
194 fn define_fn(&mut self, name: &str, sig: FnSignature) {
195 self.functions.insert(name.to_string(), sig);
196 }
197}
198
199#[derive(Debug, Clone, Default)]
202struct Refinements {
203 truthy: Vec<(String, InferredType)>,
205 falsy: Vec<(String, InferredType)>,
207}
208
209impl Refinements {
210 fn empty() -> Self {
211 Self::default()
212 }
213
214 fn inverted(self) -> Self {
216 Self {
217 truthy: self.falsy,
218 falsy: self.truthy,
219 }
220 }
221}
222
223fn builtin_return_type(name: &str) -> InferredType {
226 builtin_signatures::builtin_return_type(name)
227}
228
229fn is_builtin(name: &str) -> bool {
232 builtin_signatures::is_builtin(name)
233}
234
235pub struct TypeChecker {
237 diagnostics: Vec<TypeDiagnostic>,
238 scope: TypeScope,
239 source: Option<String>,
240}
241
242impl TypeChecker {
243 pub fn new() -> Self {
244 Self {
245 diagnostics: Vec::new(),
246 scope: TypeScope::new(),
247 source: None,
248 }
249 }
250
251 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
253 self.source = Some(source.to_string());
254 self.check_inner(program)
255 }
256
257 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
259 self.check_inner(program)
260 }
261
262 fn check_inner(mut self, program: &[SNode]) -> Vec<TypeDiagnostic> {
263 Self::register_declarations_into(&mut self.scope, program);
265
266 for snode in program {
268 if let Node::Pipeline { body, .. } = &snode.node {
269 Self::register_declarations_into(&mut self.scope, body);
270 }
271 }
272
273 for snode in program {
275 match &snode.node {
276 Node::Pipeline { params, body, .. } => {
277 let mut child = self.scope.child();
278 for p in params {
279 child.define_var(p, None);
280 }
281 self.check_block(body, &mut child);
282 }
283 Node::FnDecl {
284 name,
285 type_params,
286 params,
287 return_type,
288 where_clauses,
289 body,
290 ..
291 } => {
292 let required_params =
293 params.iter().filter(|p| p.default_value.is_none()).count();
294 let sig = FnSignature {
295 params: params
296 .iter()
297 .map(|p| (p.name.clone(), p.type_expr.clone()))
298 .collect(),
299 return_type: return_type.clone(),
300 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
301 required_params,
302 where_clauses: where_clauses
303 .iter()
304 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
305 .collect(),
306 has_rest: params.last().is_some_and(|p| p.rest),
307 };
308 self.scope.define_fn(name, sig);
309 self.check_fn_body(type_params, params, return_type, body, where_clauses);
310 }
311 _ => {
312 let mut scope = self.scope.clone();
313 self.check_node(snode, &mut scope);
314 for (name, ty) in scope.vars {
316 self.scope.vars.entry(name).or_insert(ty);
317 }
318 for name in scope.mutable_vars {
319 self.scope.mutable_vars.insert(name);
320 }
321 }
322 }
323 }
324
325 self.diagnostics
326 }
327
328 fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
330 for snode in nodes {
331 match &snode.node {
332 Node::TypeDecl { name, type_expr } => {
333 scope.type_aliases.insert(name.clone(), type_expr.clone());
334 }
335 Node::EnumDecl { name, variants, .. } => {
336 let variant_names: Vec<String> =
337 variants.iter().map(|v| v.name.clone()).collect();
338 scope.enums.insert(name.clone(), variant_names);
339 }
340 Node::InterfaceDecl { name, methods, .. } => {
341 scope.interfaces.insert(name.clone(), methods.clone());
342 }
343 Node::StructDecl { name, fields, .. } => {
344 let field_types: Vec<(String, InferredType)> = fields
345 .iter()
346 .map(|f| (f.name.clone(), f.type_expr.clone()))
347 .collect();
348 scope.structs.insert(name.clone(), field_types);
349 }
350 Node::ImplBlock {
351 type_name, methods, ..
352 } => {
353 let sigs: Vec<ImplMethodSig> = methods
354 .iter()
355 .filter_map(|m| {
356 if let Node::FnDecl {
357 name,
358 params,
359 return_type,
360 ..
361 } = &m.node
362 {
363 let non_self: Vec<_> =
364 params.iter().filter(|p| p.name != "self").collect();
365 let param_count = non_self.len();
366 let param_types: Vec<Option<TypeExpr>> =
367 non_self.iter().map(|p| p.type_expr.clone()).collect();
368 Some(ImplMethodSig {
369 name: name.clone(),
370 param_count,
371 param_types,
372 return_type: return_type.clone(),
373 })
374 } else {
375 None
376 }
377 })
378 .collect();
379 scope.impl_methods.insert(type_name.clone(), sigs);
380 }
381 _ => {}
382 }
383 }
384 }
385
386 fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
387 for stmt in stmts {
388 self.check_node(stmt, scope);
389 }
390 }
391
392 fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope, mutable: bool) {
394 let define = |scope: &mut TypeScope, name: &str| {
395 if mutable {
396 scope.define_var_mutable(name, None);
397 } else {
398 scope.define_var(name, None);
399 }
400 };
401 match pattern {
402 BindingPattern::Identifier(name) => {
403 define(scope, name);
404 }
405 BindingPattern::Dict(fields) => {
406 for field in fields {
407 let name = field.alias.as_deref().unwrap_or(&field.key);
408 define(scope, name);
409 }
410 }
411 BindingPattern::List(elements) => {
412 for elem in elements {
413 define(scope, &elem.name);
414 }
415 }
416 }
417 }
418
419 fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
420 let span = snode.span;
421 match &snode.node {
422 Node::LetBinding {
423 pattern,
424 type_ann,
425 value,
426 } => {
427 self.check_binops(value, scope);
428 let inferred = self.infer_type(value, scope);
429 if let BindingPattern::Identifier(name) = pattern {
430 if let Some(expected) = type_ann {
431 if let Some(actual) = &inferred {
432 if !self.types_compatible(expected, actual, scope) {
433 let mut msg = format!(
434 "Type mismatch: '{}' declared as {}, but assigned {}",
435 name,
436 format_type(expected),
437 format_type(actual)
438 );
439 if let Some(detail) = shape_mismatch_detail(expected, actual) {
440 msg.push_str(&format!(" ({})", detail));
441 }
442 self.error_at(msg, span);
443 }
444 }
445 }
446 let ty = type_ann.clone().or(inferred);
447 scope.define_var(name, ty);
448 } else {
449 Self::define_pattern_vars(pattern, scope, false);
450 }
451 }
452
453 Node::VarBinding {
454 pattern,
455 type_ann,
456 value,
457 } => {
458 self.check_binops(value, scope);
459 let inferred = self.infer_type(value, scope);
460 if let BindingPattern::Identifier(name) = pattern {
461 if let Some(expected) = type_ann {
462 if let Some(actual) = &inferred {
463 if !self.types_compatible(expected, actual, scope) {
464 let mut msg = format!(
465 "Type mismatch: '{}' declared as {}, but assigned {}",
466 name,
467 format_type(expected),
468 format_type(actual)
469 );
470 if let Some(detail) = shape_mismatch_detail(expected, actual) {
471 msg.push_str(&format!(" ({})", detail));
472 }
473 self.error_at(msg, span);
474 }
475 }
476 }
477 let ty = type_ann.clone().or(inferred);
478 scope.define_var_mutable(name, ty);
479 } else {
480 Self::define_pattern_vars(pattern, scope, true);
481 }
482 }
483
484 Node::FnDecl {
485 name,
486 type_params,
487 params,
488 return_type,
489 where_clauses,
490 body,
491 ..
492 } => {
493 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
494 let sig = FnSignature {
495 params: params
496 .iter()
497 .map(|p| (p.name.clone(), p.type_expr.clone()))
498 .collect(),
499 return_type: return_type.clone(),
500 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
501 required_params,
502 where_clauses: where_clauses
503 .iter()
504 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
505 .collect(),
506 has_rest: params.last().is_some_and(|p| p.rest),
507 };
508 scope.define_fn(name, sig.clone());
509 scope.define_var(name, None);
510 self.check_fn_body(type_params, params, return_type, body, where_clauses);
511 }
512
513 Node::ToolDecl {
514 name,
515 params,
516 return_type,
517 body,
518 ..
519 } => {
520 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
522 let sig = FnSignature {
523 params: params
524 .iter()
525 .map(|p| (p.name.clone(), p.type_expr.clone()))
526 .collect(),
527 return_type: return_type.clone(),
528 type_param_names: Vec::new(),
529 required_params,
530 where_clauses: Vec::new(),
531 has_rest: params.last().is_some_and(|p| p.rest),
532 };
533 scope.define_fn(name, sig);
534 scope.define_var(name, None);
535 self.check_fn_body(&[], params, return_type, body, &[]);
536 }
537
538 Node::FunctionCall { name, args } => {
539 self.check_call(name, args, scope, span);
540 }
541
542 Node::IfElse {
543 condition,
544 then_body,
545 else_body,
546 } => {
547 self.check_node(condition, scope);
548 let refs = Self::extract_refinements(condition, scope);
549
550 let mut then_scope = scope.child();
551 apply_refinements(&mut then_scope, &refs.truthy);
552 self.check_block(then_body, &mut then_scope);
553
554 if let Some(else_body) = else_body {
555 let mut else_scope = scope.child();
556 apply_refinements(&mut else_scope, &refs.falsy);
557 self.check_block(else_body, &mut else_scope);
558
559 if Self::block_definitely_exits(then_body)
562 && !Self::block_definitely_exits(else_body)
563 {
564 apply_refinements(scope, &refs.falsy);
565 } else if Self::block_definitely_exits(else_body)
566 && !Self::block_definitely_exits(then_body)
567 {
568 apply_refinements(scope, &refs.truthy);
569 }
570 } else {
571 if Self::block_definitely_exits(then_body) {
573 apply_refinements(scope, &refs.falsy);
574 }
575 }
576 }
577
578 Node::ForIn {
579 pattern,
580 iterable,
581 body,
582 } => {
583 self.check_node(iterable, scope);
584 let mut loop_scope = scope.child();
585 if let BindingPattern::Identifier(variable) = pattern {
586 let elem_type = match self.infer_type(iterable, scope) {
588 Some(TypeExpr::List(inner)) => Some(*inner),
589 Some(TypeExpr::Named(n)) if n == "string" => {
590 Some(TypeExpr::Named("string".into()))
591 }
592 _ => None,
593 };
594 loop_scope.define_var(variable, elem_type);
595 } else {
596 Self::define_pattern_vars(pattern, &mut loop_scope, false);
597 }
598 self.check_block(body, &mut loop_scope);
599 }
600
601 Node::WhileLoop { condition, body } => {
602 self.check_node(condition, scope);
603 let refs = Self::extract_refinements(condition, scope);
604 let mut loop_scope = scope.child();
605 apply_refinements(&mut loop_scope, &refs.truthy);
606 self.check_block(body, &mut loop_scope);
607 }
608
609 Node::RequireStmt { condition, message } => {
610 self.check_node(condition, scope);
611 if let Some(message) = message {
612 self.check_node(message, scope);
613 }
614 }
615
616 Node::TryCatch {
617 body,
618 error_var,
619 catch_body,
620 finally_body,
621 ..
622 } => {
623 let mut try_scope = scope.child();
624 self.check_block(body, &mut try_scope);
625 let mut catch_scope = scope.child();
626 if let Some(var) = error_var {
627 catch_scope.define_var(var, None);
628 }
629 self.check_block(catch_body, &mut catch_scope);
630 if let Some(fb) = finally_body {
631 let mut finally_scope = scope.child();
632 self.check_block(fb, &mut finally_scope);
633 }
634 }
635
636 Node::TryExpr { body } => {
637 let mut try_scope = scope.child();
638 self.check_block(body, &mut try_scope);
639 }
640
641 Node::ReturnStmt {
642 value: Some(val), ..
643 } => {
644 self.check_node(val, scope);
645 }
646
647 Node::Assignment {
648 target, value, op, ..
649 } => {
650 self.check_node(value, scope);
651 if let Node::Identifier(name) = &target.node {
652 if scope.get_var(name).is_some() && !scope.is_mutable(name) {
654 self.warning_at(
655 format!(
656 "Cannot assign to '{}': variable is immutable (declared with 'let')",
657 name
658 ),
659 span,
660 );
661 }
662
663 if let Some(Some(var_type)) = scope.get_var(name) {
664 let value_type = self.infer_type(value, scope);
665 let assigned = if let Some(op) = op {
666 let var_inferred = scope.get_var(name).cloned().flatten();
667 infer_binary_op_type(op, &var_inferred, &value_type)
668 } else {
669 value_type
670 };
671 if let Some(actual) = &assigned {
672 let check_type = scope
674 .narrowed_vars
675 .get(name)
676 .and_then(|t| t.as_ref())
677 .unwrap_or(var_type);
678 if !self.types_compatible(check_type, actual, scope) {
679 self.error_at(
680 format!(
681 "Type mismatch: cannot assign {} to '{}' (declared as {})",
682 format_type(actual),
683 name,
684 format_type(check_type)
685 ),
686 span,
687 );
688 }
689 }
690 }
691
692 if let Some(original) = scope.narrowed_vars.remove(name) {
694 scope.define_var(name, original);
695 }
696 }
697 }
698
699 Node::TypeDecl { name, type_expr } => {
700 scope.type_aliases.insert(name.clone(), type_expr.clone());
701 }
702
703 Node::EnumDecl { name, variants, .. } => {
704 let variant_names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
705 scope.enums.insert(name.clone(), variant_names);
706 }
707
708 Node::StructDecl { name, fields, .. } => {
709 let field_types: Vec<(String, InferredType)> = fields
710 .iter()
711 .map(|f| (f.name.clone(), f.type_expr.clone()))
712 .collect();
713 scope.structs.insert(name.clone(), field_types);
714 }
715
716 Node::InterfaceDecl { name, methods, .. } => {
717 scope.interfaces.insert(name.clone(), methods.clone());
718 }
719
720 Node::ImplBlock {
721 type_name, methods, ..
722 } => {
723 let sigs: Vec<ImplMethodSig> = methods
725 .iter()
726 .filter_map(|m| {
727 if let Node::FnDecl {
728 name,
729 params,
730 return_type,
731 ..
732 } = &m.node
733 {
734 let non_self: Vec<_> =
735 params.iter().filter(|p| p.name != "self").collect();
736 let param_count = non_self.len();
737 let param_types: Vec<Option<TypeExpr>> =
738 non_self.iter().map(|p| p.type_expr.clone()).collect();
739 Some(ImplMethodSig {
740 name: name.clone(),
741 param_count,
742 param_types,
743 return_type: return_type.clone(),
744 })
745 } else {
746 None
747 }
748 })
749 .collect();
750 scope.impl_methods.insert(type_name.clone(), sigs);
751 for method_sn in methods {
752 self.check_node(method_sn, scope);
753 }
754 }
755
756 Node::TryOperator { operand } => {
757 self.check_node(operand, scope);
758 }
759
760 Node::MatchExpr { value, arms } => {
761 self.check_node(value, scope);
762 let value_type = self.infer_type(value, scope);
763 for arm in arms {
764 self.check_node(&arm.pattern, scope);
765 if let Some(ref vt) = value_type {
767 let value_type_name = format_type(vt);
768 let mismatch = match &arm.pattern.node {
769 Node::StringLiteral(_) => {
770 !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
771 }
772 Node::IntLiteral(_) => {
773 !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
774 && !self.types_compatible(
775 vt,
776 &TypeExpr::Named("float".into()),
777 scope,
778 )
779 }
780 Node::FloatLiteral(_) => {
781 !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
782 && !self.types_compatible(
783 vt,
784 &TypeExpr::Named("int".into()),
785 scope,
786 )
787 }
788 Node::BoolLiteral(_) => {
789 !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
790 }
791 _ => false,
792 };
793 if mismatch {
794 let pattern_type = match &arm.pattern.node {
795 Node::StringLiteral(_) => "string",
796 Node::IntLiteral(_) => "int",
797 Node::FloatLiteral(_) => "float",
798 Node::BoolLiteral(_) => "bool",
799 _ => unreachable!(),
800 };
801 self.warning_at(
802 format!(
803 "Match pattern type mismatch: matching {} against {} literal",
804 value_type_name, pattern_type
805 ),
806 arm.pattern.span,
807 );
808 }
809 }
810 let mut arm_scope = scope.child();
811 if let Node::Identifier(var_name) = &value.node {
813 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(var_name) {
814 let narrowed = match &arm.pattern.node {
815 Node::NilLiteral => narrow_to_single(members, "nil"),
816 Node::StringLiteral(_) => narrow_to_single(members, "string"),
817 Node::IntLiteral(_) => narrow_to_single(members, "int"),
818 Node::FloatLiteral(_) => narrow_to_single(members, "float"),
819 Node::BoolLiteral(_) => narrow_to_single(members, "bool"),
820 _ => None,
821 };
822 if let Some(narrowed_type) = narrowed {
823 arm_scope.define_var(var_name, Some(narrowed_type));
824 }
825 }
826 }
827 self.check_block(&arm.body, &mut arm_scope);
828 }
829 self.check_match_exhaustiveness(value, arms, scope, span);
830 }
831
832 Node::BinaryOp { op, left, right } => {
834 self.check_node(left, scope);
835 self.check_node(right, scope);
836 let lt = self.infer_type(left, scope);
838 let rt = self.infer_type(right, scope);
839 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
840 match op.as_str() {
841 "-" | "/" | "%" => {
842 let numeric = ["int", "float"];
843 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
844 self.error_at(
845 format!(
846 "Operator '{}' requires numeric operands, got {} and {}",
847 op, l, r
848 ),
849 span,
850 );
851 }
852 }
853 "*" => {
854 let numeric = ["int", "float"];
855 let is_numeric =
856 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
857 let is_string_repeat =
858 (l == "string" && r == "int") || (l == "int" && r == "string");
859 if !is_numeric && !is_string_repeat {
860 self.error_at(
861 format!(
862 "Operator '*' requires numeric operands or string * int, got {} and {}",
863 l, r
864 ),
865 span,
866 );
867 }
868 }
869 "+" => {
870 let valid = matches!(
871 (l.as_str(), r.as_str()),
872 ("int" | "float", "int" | "float")
873 | ("string", "string")
874 | ("list", "list")
875 | ("dict", "dict")
876 );
877 if !valid {
878 let msg =
879 format!("Operator '+' is not valid for types {} and {}", l, r);
880 let fix = if l == "string" || r == "string" {
882 self.build_interpolation_fix(left, right, l == "string", span)
883 } else {
884 None
885 };
886 if let Some(fix) = fix {
887 self.error_at_with_fix(msg, span, fix);
888 } else {
889 self.error_at(msg, span);
890 }
891 }
892 }
893 "<" | ">" | "<=" | ">=" => {
894 let comparable = ["int", "float", "string"];
895 if !comparable.contains(&l.as_str())
896 || !comparable.contains(&r.as_str())
897 {
898 self.warning_at(
899 format!(
900 "Comparison '{}' may not be meaningful for types {} and {}",
901 op, l, r
902 ),
903 span,
904 );
905 } else if (l == "string") != (r == "string") {
906 self.warning_at(
907 format!(
908 "Comparing {} with {} using '{}' may give unexpected results",
909 l, r, op
910 ),
911 span,
912 );
913 }
914 }
915 _ => {}
916 }
917 }
918 }
919 Node::UnaryOp { operand, .. } => {
920 self.check_node(operand, scope);
921 }
922 Node::MethodCall {
923 object,
924 method,
925 args,
926 ..
927 }
928 | Node::OptionalMethodCall {
929 object,
930 method,
931 args,
932 ..
933 } => {
934 self.check_node(object, scope);
935 for arg in args {
936 self.check_node(arg, scope);
937 }
938 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
942 if scope.is_generic_type_param(&type_name) {
943 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
944 if let Some(iface_methods) = scope.get_interface(iface_name) {
945 let has_method = iface_methods.iter().any(|m| m.name == *method);
946 if !has_method {
947 self.warning_at(
948 format!(
949 "Method '{}' not found in interface '{}' (constraint on '{}')",
950 method, iface_name, type_name
951 ),
952 span,
953 );
954 }
955 }
956 }
957 }
958 }
959 }
960 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
961 self.check_node(object, scope);
962 }
963 Node::SubscriptAccess { object, index } => {
964 self.check_node(object, scope);
965 self.check_node(index, scope);
966 }
967 Node::SliceAccess { object, start, end } => {
968 self.check_node(object, scope);
969 if let Some(s) = start {
970 self.check_node(s, scope);
971 }
972 if let Some(e) = end {
973 self.check_node(e, scope);
974 }
975 }
976
977 Node::Ternary {
979 condition,
980 true_expr,
981 false_expr,
982 } => {
983 self.check_node(condition, scope);
984 let refs = Self::extract_refinements(condition, scope);
985
986 let mut true_scope = scope.child();
987 apply_refinements(&mut true_scope, &refs.truthy);
988 self.check_node(true_expr, &mut true_scope);
989
990 let mut false_scope = scope.child();
991 apply_refinements(&mut false_scope, &refs.falsy);
992 self.check_node(false_expr, &mut false_scope);
993 }
994
995 Node::ThrowStmt { value } => {
996 self.check_node(value, scope);
997 }
998
999 Node::GuardStmt {
1000 condition,
1001 else_body,
1002 } => {
1003 self.check_node(condition, scope);
1004 let refs = Self::extract_refinements(condition, scope);
1005
1006 let mut else_scope = scope.child();
1007 apply_refinements(&mut else_scope, &refs.falsy);
1008 self.check_block(else_body, &mut else_scope);
1009
1010 apply_refinements(scope, &refs.truthy);
1013 }
1014
1015 Node::SpawnExpr { body } => {
1016 let mut spawn_scope = scope.child();
1017 self.check_block(body, &mut spawn_scope);
1018 }
1019
1020 Node::Parallel {
1021 count,
1022 variable,
1023 body,
1024 } => {
1025 self.check_node(count, scope);
1026 let mut par_scope = scope.child();
1027 if let Some(var) = variable {
1028 par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
1029 }
1030 self.check_block(body, &mut par_scope);
1031 }
1032
1033 Node::ParallelMap {
1034 list,
1035 variable,
1036 body,
1037 }
1038 | Node::ParallelSettle {
1039 list,
1040 variable,
1041 body,
1042 } => {
1043 self.check_node(list, scope);
1044 let mut par_scope = scope.child();
1045 let elem_type = match self.infer_type(list, scope) {
1046 Some(TypeExpr::List(inner)) => Some(*inner),
1047 _ => None,
1048 };
1049 par_scope.define_var(variable, elem_type);
1050 self.check_block(body, &mut par_scope);
1051 }
1052
1053 Node::SelectExpr {
1054 cases,
1055 timeout,
1056 default_body,
1057 } => {
1058 for case in cases {
1059 self.check_node(&case.channel, scope);
1060 let mut case_scope = scope.child();
1061 case_scope.define_var(&case.variable, None);
1062 self.check_block(&case.body, &mut case_scope);
1063 }
1064 if let Some((dur, body)) = timeout {
1065 self.check_node(dur, scope);
1066 let mut timeout_scope = scope.child();
1067 self.check_block(body, &mut timeout_scope);
1068 }
1069 if let Some(body) = default_body {
1070 let mut default_scope = scope.child();
1071 self.check_block(body, &mut default_scope);
1072 }
1073 }
1074
1075 Node::DeadlineBlock { duration, body } => {
1076 self.check_node(duration, scope);
1077 let mut block_scope = scope.child();
1078 self.check_block(body, &mut block_scope);
1079 }
1080
1081 Node::MutexBlock { body } => {
1082 let mut block_scope = scope.child();
1083 self.check_block(body, &mut block_scope);
1084 }
1085
1086 Node::Retry { count, body } => {
1087 self.check_node(count, scope);
1088 let mut retry_scope = scope.child();
1089 self.check_block(body, &mut retry_scope);
1090 }
1091
1092 Node::Closure { params, body, .. } => {
1093 let mut closure_scope = scope.child();
1094 for p in params {
1095 closure_scope.define_var(&p.name, p.type_expr.clone());
1096 }
1097 self.check_block(body, &mut closure_scope);
1098 }
1099
1100 Node::ListLiteral(elements) => {
1101 for elem in elements {
1102 self.check_node(elem, scope);
1103 }
1104 }
1105
1106 Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
1107 for entry in entries {
1108 self.check_node(&entry.key, scope);
1109 self.check_node(&entry.value, scope);
1110 }
1111 }
1112
1113 Node::RangeExpr { start, end, .. } => {
1114 self.check_node(start, scope);
1115 self.check_node(end, scope);
1116 }
1117
1118 Node::Spread(inner) => {
1119 self.check_node(inner, scope);
1120 }
1121
1122 Node::Block(stmts) => {
1123 let mut block_scope = scope.child();
1124 self.check_block(stmts, &mut block_scope);
1125 }
1126
1127 Node::YieldExpr { value } => {
1128 if let Some(v) = value {
1129 self.check_node(v, scope);
1130 }
1131 }
1132
1133 Node::StructConstruct {
1135 struct_name,
1136 fields,
1137 } => {
1138 for entry in fields {
1139 self.check_node(&entry.key, scope);
1140 self.check_node(&entry.value, scope);
1141 }
1142 if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
1143 for entry in fields {
1145 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1146 if !declared_fields.iter().any(|(name, _)| name == key) {
1147 self.warning_at(
1148 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1149 entry.key.span,
1150 );
1151 }
1152 }
1153 }
1154 let provided: Vec<String> = fields
1156 .iter()
1157 .filter_map(|e| match &e.key.node {
1158 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1159 _ => None,
1160 })
1161 .collect();
1162 for (name, _) in &declared_fields {
1163 if !provided.contains(name) {
1164 self.warning_at(
1165 format!(
1166 "Missing field '{}' in struct '{}' construction",
1167 name, struct_name
1168 ),
1169 span,
1170 );
1171 }
1172 }
1173 }
1174 }
1175
1176 Node::EnumConstruct {
1178 enum_name,
1179 variant,
1180 args,
1181 } => {
1182 for arg in args {
1183 self.check_node(arg, scope);
1184 }
1185 if let Some(variants) = scope.get_enum(enum_name) {
1186 if !variants.contains(variant) {
1187 self.warning_at(
1188 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1189 span,
1190 );
1191 }
1192 }
1193 }
1194
1195 Node::InterpolatedString(_) => {}
1197
1198 Node::StringLiteral(_)
1200 | Node::RawStringLiteral(_)
1201 | Node::IntLiteral(_)
1202 | Node::FloatLiteral(_)
1203 | Node::BoolLiteral(_)
1204 | Node::NilLiteral
1205 | Node::Identifier(_)
1206 | Node::DurationLiteral(_)
1207 | Node::BreakStmt
1208 | Node::ContinueStmt
1209 | Node::ReturnStmt { value: None }
1210 | Node::ImportDecl { .. }
1211 | Node::SelectiveImport { .. } => {}
1212
1213 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1216 let mut decl_scope = scope.child();
1217 self.check_block(body, &mut decl_scope);
1218 }
1219 }
1220 }
1221
1222 fn check_fn_body(
1223 &mut self,
1224 type_params: &[TypeParam],
1225 params: &[TypedParam],
1226 return_type: &Option<TypeExpr>,
1227 body: &[SNode],
1228 where_clauses: &[WhereClause],
1229 ) {
1230 let mut fn_scope = self.scope.child();
1231 for tp in type_params {
1234 fn_scope.generic_type_params.insert(tp.name.clone());
1235 }
1236 for wc in where_clauses {
1238 fn_scope
1239 .where_constraints
1240 .insert(wc.type_name.clone(), wc.bound.clone());
1241 }
1242 for param in params {
1243 fn_scope.define_var(¶m.name, param.type_expr.clone());
1244 if let Some(default) = ¶m.default_value {
1245 self.check_node(default, &mut fn_scope);
1246 }
1247 }
1248 let ret_scope_base = if return_type.is_some() {
1251 Some(fn_scope.child())
1252 } else {
1253 None
1254 };
1255
1256 self.check_block(body, &mut fn_scope);
1257
1258 if let Some(ret_type) = return_type {
1260 let mut ret_scope = ret_scope_base.unwrap();
1261 for stmt in body {
1262 self.check_return_type(stmt, ret_type, &mut ret_scope);
1263 }
1264 }
1265 }
1266
1267 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1268 let span = snode.span;
1269 match &snode.node {
1270 Node::ReturnStmt { value: Some(val) } => {
1271 let inferred = self.infer_type(val, scope);
1272 if let Some(actual) = &inferred {
1273 if !self.types_compatible(expected, actual, scope) {
1274 self.error_at(
1275 format!(
1276 "Return type mismatch: expected {}, got {}",
1277 format_type(expected),
1278 format_type(actual)
1279 ),
1280 span,
1281 );
1282 }
1283 }
1284 }
1285 Node::IfElse {
1286 condition,
1287 then_body,
1288 else_body,
1289 } => {
1290 let refs = Self::extract_refinements(condition, scope);
1291 let mut then_scope = scope.child();
1292 apply_refinements(&mut then_scope, &refs.truthy);
1293 for stmt in then_body {
1294 self.check_return_type(stmt, expected, &mut then_scope);
1295 }
1296 if let Some(else_body) = else_body {
1297 let mut else_scope = scope.child();
1298 apply_refinements(&mut else_scope, &refs.falsy);
1299 for stmt in else_body {
1300 self.check_return_type(stmt, expected, &mut else_scope);
1301 }
1302 if Self::block_definitely_exits(then_body)
1304 && !Self::block_definitely_exits(else_body)
1305 {
1306 apply_refinements(scope, &refs.falsy);
1307 } else if Self::block_definitely_exits(else_body)
1308 && !Self::block_definitely_exits(then_body)
1309 {
1310 apply_refinements(scope, &refs.truthy);
1311 }
1312 } else {
1313 if Self::block_definitely_exits(then_body) {
1315 apply_refinements(scope, &refs.falsy);
1316 }
1317 }
1318 }
1319 _ => {}
1320 }
1321 }
1322
1323 fn satisfies_interface(
1329 &self,
1330 type_name: &str,
1331 interface_name: &str,
1332 scope: &TypeScope,
1333 ) -> bool {
1334 self.interface_mismatch_reason(type_name, interface_name, scope)
1335 .is_none()
1336 }
1337
1338 fn interface_mismatch_reason(
1341 &self,
1342 type_name: &str,
1343 interface_name: &str,
1344 scope: &TypeScope,
1345 ) -> Option<String> {
1346 let interface_methods = match scope.get_interface(interface_name) {
1347 Some(methods) => methods,
1348 None => return Some(format!("interface '{}' not found", interface_name)),
1349 };
1350 let impl_methods = match scope.get_impl_methods(type_name) {
1351 Some(methods) => methods,
1352 None => {
1353 if interface_methods.is_empty() {
1354 return None;
1355 }
1356 let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1357 return Some(format!("missing method(s): {}", names.join(", ")));
1358 }
1359 };
1360 for iface_method in interface_methods {
1361 let iface_params: Vec<_> = iface_method
1362 .params
1363 .iter()
1364 .filter(|p| p.name != "self")
1365 .collect();
1366 let iface_param_count = iface_params.len();
1367 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1368 let impl_method = match matching_impl {
1369 Some(m) => m,
1370 None => {
1371 return Some(format!("missing method '{}'", iface_method.name));
1372 }
1373 };
1374 if impl_method.param_count != iface_param_count {
1375 return Some(format!(
1376 "method '{}' has {} parameter(s), expected {}",
1377 iface_method.name, impl_method.param_count, iface_param_count
1378 ));
1379 }
1380 for (i, iface_param) in iface_params.iter().enumerate() {
1382 if let (Some(expected), Some(actual)) = (
1383 &iface_param.type_expr,
1384 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1385 ) {
1386 if !self.types_compatible(expected, actual, scope) {
1387 return Some(format!(
1388 "method '{}' parameter {} has type '{}', expected '{}'",
1389 iface_method.name,
1390 i + 1,
1391 format_type(actual),
1392 format_type(expected),
1393 ));
1394 }
1395 }
1396 }
1397 if let (Some(expected_ret), Some(actual_ret)) =
1399 (&iface_method.return_type, &impl_method.return_type)
1400 {
1401 if !self.types_compatible(expected_ret, actual_ret, scope) {
1402 return Some(format!(
1403 "method '{}' returns '{}', expected '{}'",
1404 iface_method.name,
1405 format_type(actual_ret),
1406 format_type(expected_ret),
1407 ));
1408 }
1409 }
1410 }
1411 None
1412 }
1413
1414 fn extract_type_bindings(
1417 param_type: &TypeExpr,
1418 arg_type: &TypeExpr,
1419 type_params: &std::collections::BTreeSet<String>,
1420 bindings: &mut BTreeMap<String, String>,
1421 ) {
1422 match (param_type, arg_type) {
1423 (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1425 if type_params.contains(param_name) =>
1426 {
1427 bindings
1428 .entry(param_name.clone())
1429 .or_insert(concrete.clone());
1430 }
1431 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1433 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1434 }
1435 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1437 Self::extract_type_bindings(pk, ak, type_params, bindings);
1438 Self::extract_type_bindings(pv, av, type_params, bindings);
1439 }
1440 _ => {}
1441 }
1442 }
1443
1444 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1446 match &condition.node {
1447 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1449 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1450 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1451 return nil_ref;
1452 }
1453 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1454 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1455 return typeof_ref;
1456 }
1457 Refinements::empty()
1458 }
1459
1460 Node::BinaryOp { op, left, right } if op == "&&" => {
1462 let left_ref = Self::extract_refinements(left, scope);
1463 let right_ref = Self::extract_refinements(right, scope);
1464 let mut truthy = left_ref.truthy;
1465 truthy.extend(right_ref.truthy);
1466 Refinements {
1467 truthy,
1468 falsy: vec![],
1469 }
1470 }
1471
1472 Node::BinaryOp { op, left, right } if op == "||" => {
1474 let left_ref = Self::extract_refinements(left, scope);
1475 let right_ref = Self::extract_refinements(right, scope);
1476 let mut falsy = left_ref.falsy;
1477 falsy.extend(right_ref.falsy);
1478 Refinements {
1479 truthy: vec![],
1480 falsy,
1481 }
1482 }
1483
1484 Node::UnaryOp { op, operand } if op == "!" => {
1486 Self::extract_refinements(operand, scope).inverted()
1487 }
1488
1489 Node::Identifier(name) => {
1491 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1492 if members
1493 .iter()
1494 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1495 {
1496 if let Some(narrowed) = remove_from_union(members, "nil") {
1497 return Refinements {
1498 truthy: vec![(name.clone(), Some(narrowed))],
1499 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1500 };
1501 }
1502 }
1503 }
1504 Refinements::empty()
1505 }
1506
1507 Node::MethodCall {
1509 object,
1510 method,
1511 args,
1512 } if method == "has" && args.len() == 1 => {
1513 Self::extract_has_refinements(object, args, scope)
1514 }
1515
1516 _ => Refinements::empty(),
1517 }
1518 }
1519
1520 fn extract_nil_refinements(
1522 op: &str,
1523 left: &SNode,
1524 right: &SNode,
1525 scope: &TypeScope,
1526 ) -> Refinements {
1527 let var_node = if matches!(right.node, Node::NilLiteral) {
1528 left
1529 } else if matches!(left.node, Node::NilLiteral) {
1530 right
1531 } else {
1532 return Refinements::empty();
1533 };
1534
1535 if let Node::Identifier(name) = &var_node.node {
1536 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1537 if let Some(narrowed) = remove_from_union(members, "nil") {
1538 let neq_refs = Refinements {
1539 truthy: vec![(name.clone(), Some(narrowed))],
1540 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1541 };
1542 return if op == "!=" {
1543 neq_refs
1544 } else {
1545 neq_refs.inverted()
1546 };
1547 }
1548 }
1549 }
1550 Refinements::empty()
1551 }
1552
1553 fn extract_typeof_refinements(
1555 op: &str,
1556 left: &SNode,
1557 right: &SNode,
1558 scope: &TypeScope,
1559 ) -> Refinements {
1560 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1561 (extract_type_of_var(left), &right.node)
1562 {
1563 (var, tn.clone())
1564 } else if let (Node::StringLiteral(tn), Some(var)) =
1565 (&left.node, extract_type_of_var(right))
1566 {
1567 (var, tn.clone())
1568 } else {
1569 return Refinements::empty();
1570 };
1571
1572 const KNOWN_TYPES: &[&str] = &[
1573 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1574 ];
1575 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1576 return Refinements::empty();
1577 }
1578
1579 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(&var_name) {
1580 let narrowed = narrow_to_single(members, &type_name);
1581 let remaining = remove_from_union(members, &type_name);
1582 if narrowed.is_some() || remaining.is_some() {
1583 let eq_refs = Refinements {
1584 truthy: narrowed
1585 .map(|n| vec![(var_name.clone(), Some(n))])
1586 .unwrap_or_default(),
1587 falsy: remaining
1588 .map(|r| vec![(var_name.clone(), Some(r))])
1589 .unwrap_or_default(),
1590 };
1591 return if op == "==" {
1592 eq_refs
1593 } else {
1594 eq_refs.inverted()
1595 };
1596 }
1597 }
1598 Refinements::empty()
1599 }
1600
1601 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1603 if let Node::Identifier(var_name) = &object.node {
1604 if let Node::StringLiteral(key) = &args[0].node {
1605 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1606 if fields.iter().any(|f| f.name == *key && f.optional) {
1607 let narrowed_fields: Vec<ShapeField> = fields
1608 .iter()
1609 .map(|f| {
1610 if f.name == *key {
1611 ShapeField {
1612 name: f.name.clone(),
1613 type_expr: f.type_expr.clone(),
1614 optional: false,
1615 }
1616 } else {
1617 f.clone()
1618 }
1619 })
1620 .collect();
1621 return Refinements {
1622 truthy: vec![(
1623 var_name.clone(),
1624 Some(TypeExpr::Shape(narrowed_fields)),
1625 )],
1626 falsy: vec![],
1627 };
1628 }
1629 }
1630 }
1631 }
1632 Refinements::empty()
1633 }
1634
1635 fn block_definitely_exits(stmts: &[SNode]) -> bool {
1637 stmts.iter().any(|s| match &s.node {
1638 Node::ReturnStmt { .. }
1639 | Node::ThrowStmt { .. }
1640 | Node::BreakStmt
1641 | Node::ContinueStmt => true,
1642 Node::IfElse {
1643 then_body,
1644 else_body: Some(else_body),
1645 ..
1646 } => Self::block_definitely_exits(then_body) && Self::block_definitely_exits(else_body),
1647 _ => false,
1648 })
1649 }
1650
1651 fn check_match_exhaustiveness(
1652 &mut self,
1653 value: &SNode,
1654 arms: &[MatchArm],
1655 scope: &TypeScope,
1656 span: Span,
1657 ) {
1658 let enum_name = match &value.node {
1660 Node::PropertyAccess { object, property } if property == "variant" => {
1661 match self.infer_type(object, scope) {
1663 Some(TypeExpr::Named(name)) => {
1664 if scope.get_enum(&name).is_some() {
1665 Some(name)
1666 } else {
1667 None
1668 }
1669 }
1670 _ => None,
1671 }
1672 }
1673 _ => {
1674 match self.infer_type(value, scope) {
1676 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1677 _ => None,
1678 }
1679 }
1680 };
1681
1682 let Some(enum_name) = enum_name else {
1683 return;
1684 };
1685 let Some(variants) = scope.get_enum(&enum_name) else {
1686 return;
1687 };
1688
1689 let mut covered: Vec<String> = Vec::new();
1691 let mut has_wildcard = false;
1692
1693 for arm in arms {
1694 match &arm.pattern.node {
1695 Node::StringLiteral(s) => covered.push(s.clone()),
1697 Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1699 has_wildcard = true;
1700 }
1701 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1703 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1705 _ => {
1706 has_wildcard = true;
1708 }
1709 }
1710 }
1711
1712 if has_wildcard {
1713 return;
1714 }
1715
1716 let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1717 if !missing.is_empty() {
1718 let missing_str = missing
1719 .iter()
1720 .map(|s| format!("\"{}\"", s))
1721 .collect::<Vec<_>>()
1722 .join(", ");
1723 self.warning_at(
1724 format!(
1725 "Non-exhaustive match on enum {}: missing variants {}",
1726 enum_name, missing_str
1727 ),
1728 span,
1729 );
1730 }
1731 }
1732
1733 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1734 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1736 if let Some(sig) = scope.get_fn(name).cloned() {
1737 if !has_spread
1738 && !is_builtin(name)
1739 && !sig.has_rest
1740 && (args.len() < sig.required_params || args.len() > sig.params.len())
1741 {
1742 let expected = if sig.required_params == sig.params.len() {
1743 format!("{}", sig.params.len())
1744 } else {
1745 format!("{}-{}", sig.required_params, sig.params.len())
1746 };
1747 self.warning_at(
1748 format!(
1749 "Function '{}' expects {} arguments, got {}",
1750 name,
1751 expected,
1752 args.len()
1753 ),
1754 span,
1755 );
1756 }
1757 let call_scope = if sig.type_param_names.is_empty() {
1760 scope.clone()
1761 } else {
1762 let mut s = scope.child();
1763 for tp_name in &sig.type_param_names {
1764 s.generic_type_params.insert(tp_name.clone());
1765 }
1766 s
1767 };
1768 for (i, (arg, (param_name, param_type))) in
1769 args.iter().zip(sig.params.iter()).enumerate()
1770 {
1771 if let Some(expected) = param_type {
1772 let actual = self.infer_type(arg, scope);
1773 if let Some(actual) = &actual {
1774 if !self.types_compatible(expected, actual, &call_scope) {
1775 self.error_at(
1776 format!(
1777 "Argument {} ('{}'): expected {}, got {}",
1778 i + 1,
1779 param_name,
1780 format_type(expected),
1781 format_type(actual)
1782 ),
1783 arg.span,
1784 );
1785 }
1786 }
1787 }
1788 }
1789 if !sig.where_clauses.is_empty() {
1791 let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1794 let type_param_set: std::collections::BTreeSet<String> =
1795 sig.type_param_names.iter().cloned().collect();
1796 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1797 if let Some(param_ty) = param_type {
1798 if let Some(arg_ty) = self.infer_type(arg, scope) {
1799 Self::extract_type_bindings(
1800 param_ty,
1801 &arg_ty,
1802 &type_param_set,
1803 &mut type_bindings,
1804 );
1805 }
1806 }
1807 }
1808 for (type_param, bound) in &sig.where_clauses {
1809 if let Some(concrete_type) = type_bindings.get(type_param) {
1810 if let Some(reason) =
1811 self.interface_mismatch_reason(concrete_type, bound, scope)
1812 {
1813 self.warning_at(
1814 format!(
1815 "Type '{}' does not satisfy interface '{}': {} \
1816 (required by constraint `where {}: {}`)",
1817 concrete_type, bound, reason, type_param, bound
1818 ),
1819 span,
1820 );
1821 }
1822 }
1823 }
1824 }
1825 }
1826 for arg in args {
1828 self.check_node(arg, scope);
1829 }
1830 }
1831
1832 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1834 match &snode.node {
1835 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1836 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1837 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1838 Some(TypeExpr::Named("string".into()))
1839 }
1840 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1841 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1842 Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1843 Node::DictLiteral(entries) => {
1844 let mut fields = Vec::new();
1846 let mut all_string_keys = true;
1847 for entry in entries {
1848 if let Node::StringLiteral(key) = &entry.key.node {
1849 let val_type = self
1850 .infer_type(&entry.value, scope)
1851 .unwrap_or(TypeExpr::Named("nil".into()));
1852 fields.push(ShapeField {
1853 name: key.clone(),
1854 type_expr: val_type,
1855 optional: false,
1856 });
1857 } else {
1858 all_string_keys = false;
1859 break;
1860 }
1861 }
1862 if all_string_keys && !fields.is_empty() {
1863 Some(TypeExpr::Shape(fields))
1864 } else {
1865 Some(TypeExpr::Named("dict".into()))
1866 }
1867 }
1868 Node::Closure { params, body, .. } => {
1869 let all_typed = params.iter().all(|p| p.type_expr.is_some());
1871 if all_typed && !params.is_empty() {
1872 let param_types: Vec<TypeExpr> =
1873 params.iter().filter_map(|p| p.type_expr.clone()).collect();
1874 let ret = body.last().and_then(|last| self.infer_type(last, scope));
1876 if let Some(ret_type) = ret {
1877 return Some(TypeExpr::FnType {
1878 params: param_types,
1879 return_type: Box::new(ret_type),
1880 });
1881 }
1882 }
1883 Some(TypeExpr::Named("closure".into()))
1884 }
1885
1886 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1887
1888 Node::FunctionCall { name, .. } => {
1889 if scope.get_struct(name).is_some() {
1891 return Some(TypeExpr::Named(name.clone()));
1892 }
1893 if let Some(sig) = scope.get_fn(name) {
1895 return sig.return_type.clone();
1896 }
1897 builtin_return_type(name)
1899 }
1900
1901 Node::BinaryOp { op, left, right } => {
1902 let lt = self.infer_type(left, scope);
1903 let rt = self.infer_type(right, scope);
1904 infer_binary_op_type(op, <, &rt)
1905 }
1906
1907 Node::UnaryOp { op, operand } => {
1908 let t = self.infer_type(operand, scope);
1909 match op.as_str() {
1910 "!" => Some(TypeExpr::Named("bool".into())),
1911 "-" => t, _ => None,
1913 }
1914 }
1915
1916 Node::Ternary {
1917 condition,
1918 true_expr,
1919 false_expr,
1920 } => {
1921 let refs = Self::extract_refinements(condition, scope);
1922
1923 let mut true_scope = scope.child();
1924 apply_refinements(&mut true_scope, &refs.truthy);
1925 let tt = self.infer_type(true_expr, &true_scope);
1926
1927 let mut false_scope = scope.child();
1928 apply_refinements(&mut false_scope, &refs.falsy);
1929 let ft = self.infer_type(false_expr, &false_scope);
1930
1931 match (&tt, &ft) {
1932 (Some(a), Some(b)) if a == b => tt,
1933 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1934 (Some(_), None) => tt,
1935 (None, Some(_)) => ft,
1936 (None, None) => None,
1937 }
1938 }
1939
1940 Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1941
1942 Node::PropertyAccess { object, property } => {
1943 if let Node::Identifier(name) = &object.node {
1945 if scope.get_enum(name).is_some() {
1946 return Some(TypeExpr::Named(name.clone()));
1947 }
1948 }
1949 if property == "variant" {
1951 let obj_type = self.infer_type(object, scope);
1952 if let Some(TypeExpr::Named(name)) = &obj_type {
1953 if scope.get_enum(name).is_some() {
1954 return Some(TypeExpr::Named("string".into()));
1955 }
1956 }
1957 }
1958 let obj_type = self.infer_type(object, scope);
1960 if let Some(TypeExpr::Shape(fields)) = &obj_type {
1961 if let Some(field) = fields.iter().find(|f| f.name == *property) {
1962 return Some(field.type_expr.clone());
1963 }
1964 }
1965 None
1966 }
1967
1968 Node::SubscriptAccess { object, index } => {
1969 let obj_type = self.infer_type(object, scope);
1970 match &obj_type {
1971 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1972 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1973 Some(TypeExpr::Shape(fields)) => {
1974 if let Node::StringLiteral(key) = &index.node {
1976 fields
1977 .iter()
1978 .find(|f| &f.name == key)
1979 .map(|f| f.type_expr.clone())
1980 } else {
1981 None
1982 }
1983 }
1984 Some(TypeExpr::Named(n)) if n == "list" => None,
1985 Some(TypeExpr::Named(n)) if n == "dict" => None,
1986 Some(TypeExpr::Named(n)) if n == "string" => {
1987 Some(TypeExpr::Named("string".into()))
1988 }
1989 _ => None,
1990 }
1991 }
1992 Node::SliceAccess { object, .. } => {
1993 let obj_type = self.infer_type(object, scope);
1995 match &obj_type {
1996 Some(TypeExpr::List(_)) => obj_type,
1997 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1998 Some(TypeExpr::Named(n)) if n == "string" => {
1999 Some(TypeExpr::Named("string".into()))
2000 }
2001 _ => None,
2002 }
2003 }
2004 Node::MethodCall { object, method, .. }
2005 | Node::OptionalMethodCall { object, method, .. } => {
2006 let obj_type = self.infer_type(object, scope);
2007 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
2008 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
2009 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
2010 match method.as_str() {
2011 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
2013 Some(TypeExpr::Named("bool".into()))
2014 }
2015 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
2017 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
2019 | "pad_left" | "pad_right" | "repeat" | "join" => {
2020 Some(TypeExpr::Named("string".into()))
2021 }
2022 "split" | "chars" => Some(TypeExpr::Named("list".into())),
2023 "filter" => {
2025 if is_dict {
2026 Some(TypeExpr::Named("dict".into()))
2027 } else {
2028 Some(TypeExpr::Named("list".into()))
2029 }
2030 }
2031 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2033 "reduce" | "find" | "first" | "last" => None,
2034 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2036 "merge" | "map_values" | "rekey" | "map_keys" => {
2037 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2041 Some(TypeExpr::DictType(
2042 Box::new(TypeExpr::Named("string".into())),
2043 v.clone(),
2044 ))
2045 } else {
2046 Some(TypeExpr::Named("dict".into()))
2047 }
2048 }
2049 "to_string" => Some(TypeExpr::Named("string".into())),
2051 "to_int" => Some(TypeExpr::Named("int".into())),
2052 "to_float" => Some(TypeExpr::Named("float".into())),
2053 _ => None,
2054 }
2055 }
2056
2057 Node::TryOperator { operand } => {
2059 match self.infer_type(operand, scope) {
2060 Some(TypeExpr::Named(name)) if name == "Result" => None, _ => None,
2062 }
2063 }
2064
2065 _ => None,
2066 }
2067 }
2068
2069 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2071 if let TypeExpr::Named(name) = expected {
2073 if scope.is_generic_type_param(name) {
2074 return true;
2075 }
2076 }
2077 if let TypeExpr::Named(name) = actual {
2078 if scope.is_generic_type_param(name) {
2079 return true;
2080 }
2081 }
2082 let expected = self.resolve_alias(expected, scope);
2083 let actual = self.resolve_alias(actual, scope);
2084
2085 if let TypeExpr::Named(iface_name) = &expected {
2088 if scope.get_interface(iface_name).is_some() {
2089 if let TypeExpr::Named(type_name) = &actual {
2090 return self.satisfies_interface(type_name, iface_name, scope);
2091 }
2092 return false;
2093 }
2094 }
2095
2096 match (&expected, &actual) {
2097 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2098 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2101 act_members.iter().all(|am| {
2102 exp_members
2103 .iter()
2104 .any(|em| self.types_compatible(em, am, scope))
2105 })
2106 }
2107 (TypeExpr::Union(members), actual_type) => members
2108 .iter()
2109 .any(|m| self.types_compatible(m, actual_type, scope)),
2110 (expected_type, TypeExpr::Union(members)) => members
2111 .iter()
2112 .all(|m| self.types_compatible(expected_type, m, scope)),
2113 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2114 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2115 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2116 if expected_field.optional {
2117 return true;
2118 }
2119 af.iter().any(|actual_field| {
2120 actual_field.name == expected_field.name
2121 && self.types_compatible(
2122 &expected_field.type_expr,
2123 &actual_field.type_expr,
2124 scope,
2125 )
2126 })
2127 }),
2128 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2130 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2131 keys_ok
2132 && af
2133 .iter()
2134 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2135 }
2136 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2138 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2139 self.types_compatible(expected_inner, actual_inner, scope)
2140 }
2141 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2142 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2143 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2144 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2145 }
2146 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2147 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2148 (
2150 TypeExpr::FnType {
2151 params: ep,
2152 return_type: er,
2153 },
2154 TypeExpr::FnType {
2155 params: ap,
2156 return_type: ar,
2157 },
2158 ) => {
2159 ep.len() == ap.len()
2160 && ep
2161 .iter()
2162 .zip(ap.iter())
2163 .all(|(e, a)| self.types_compatible(e, a, scope))
2164 && self.types_compatible(er, ar, scope)
2165 }
2166 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2168 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2169 _ => false,
2170 }
2171 }
2172
2173 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2174 if let TypeExpr::Named(name) = ty {
2175 if let Some(resolved) = scope.resolve_type(name) {
2176 return resolved.clone();
2177 }
2178 }
2179 ty.clone()
2180 }
2181
2182 fn error_at(&mut self, message: String, span: Span) {
2183 self.diagnostics.push(TypeDiagnostic {
2184 message,
2185 severity: DiagnosticSeverity::Error,
2186 span: Some(span),
2187 help: None,
2188 fix: None,
2189 });
2190 }
2191
2192 #[allow(dead_code)]
2193 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2194 self.diagnostics.push(TypeDiagnostic {
2195 message,
2196 severity: DiagnosticSeverity::Error,
2197 span: Some(span),
2198 help: Some(help),
2199 fix: None,
2200 });
2201 }
2202
2203 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
2204 self.diagnostics.push(TypeDiagnostic {
2205 message,
2206 severity: DiagnosticSeverity::Error,
2207 span: Some(span),
2208 help: None,
2209 fix: Some(fix),
2210 });
2211 }
2212
2213 fn warning_at(&mut self, message: String, span: Span) {
2214 self.diagnostics.push(TypeDiagnostic {
2215 message,
2216 severity: DiagnosticSeverity::Warning,
2217 span: Some(span),
2218 help: None,
2219 fix: None,
2220 });
2221 }
2222
2223 #[allow(dead_code)]
2224 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2225 self.diagnostics.push(TypeDiagnostic {
2226 message,
2227 severity: DiagnosticSeverity::Warning,
2228 span: Some(span),
2229 help: Some(help),
2230 fix: None,
2231 });
2232 }
2233
2234 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
2238 match &snode.node {
2239 Node::BinaryOp { op, left, right } => {
2240 self.check_binops(left, scope);
2241 self.check_binops(right, scope);
2242 let lt = self.infer_type(left, scope);
2243 let rt = self.infer_type(right, scope);
2244 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
2245 let span = snode.span;
2246 match op.as_str() {
2247 "+" => {
2248 let valid = matches!(
2249 (l.as_str(), r.as_str()),
2250 ("int" | "float", "int" | "float")
2251 | ("string", "string")
2252 | ("list", "list")
2253 | ("dict", "dict")
2254 );
2255 if !valid {
2256 let msg =
2257 format!("Operator '+' is not valid for types {} and {}", l, r);
2258 let fix = if l == "string" || r == "string" {
2259 self.build_interpolation_fix(left, right, l == "string", span)
2260 } else {
2261 None
2262 };
2263 if let Some(fix) = fix {
2264 self.error_at_with_fix(msg, span, fix);
2265 } else {
2266 self.error_at(msg, span);
2267 }
2268 }
2269 }
2270 "-" | "/" | "%" => {
2271 let numeric = ["int", "float"];
2272 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
2273 self.error_at(
2274 format!(
2275 "Operator '{}' requires numeric operands, got {} and {}",
2276 op, l, r
2277 ),
2278 span,
2279 );
2280 }
2281 }
2282 "*" => {
2283 let numeric = ["int", "float"];
2284 let is_numeric =
2285 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
2286 let is_string_repeat =
2287 (l == "string" && r == "int") || (l == "int" && r == "string");
2288 if !is_numeric && !is_string_repeat {
2289 self.error_at(
2290 format!(
2291 "Operator '*' requires numeric operands or string * int, got {} and {}",
2292 l, r
2293 ),
2294 span,
2295 );
2296 }
2297 }
2298 _ => {}
2299 }
2300 }
2301 }
2302 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
2304 _ => {}
2305 }
2306 }
2307
2308 fn build_interpolation_fix(
2310 &self,
2311 left: &SNode,
2312 right: &SNode,
2313 left_is_string: bool,
2314 expr_span: Span,
2315 ) -> Option<Vec<FixEdit>> {
2316 let src = self.source.as_ref()?;
2317 let (str_node, other_node) = if left_is_string {
2318 (left, right)
2319 } else {
2320 (right, left)
2321 };
2322 let str_text = src.get(str_node.span.start..str_node.span.end)?;
2323 let other_text = src.get(other_node.span.start..other_node.span.end)?;
2324 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
2326 if other_text.contains('}') || other_text.contains('"') {
2328 return None;
2329 }
2330 let replacement = if left_is_string {
2331 format!("\"{inner}${{{other_text}}}\"")
2332 } else {
2333 format!("\"${{{other_text}}}{inner}\"")
2334 };
2335 Some(vec![FixEdit {
2336 span: expr_span,
2337 replacement,
2338 }])
2339 }
2340}
2341
2342impl Default for TypeChecker {
2343 fn default() -> Self {
2344 Self::new()
2345 }
2346}
2347
2348fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2350 match op {
2351 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2352 Some(TypeExpr::Named("bool".into()))
2353 }
2354 "+" => match (left, right) {
2355 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2356 match (l.as_str(), r.as_str()) {
2357 ("int", "int") => Some(TypeExpr::Named("int".into())),
2358 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2359 ("string", "string") => Some(TypeExpr::Named("string".into())),
2360 ("list", "list") => Some(TypeExpr::Named("list".into())),
2361 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2362 _ => None,
2363 }
2364 }
2365 _ => None,
2366 },
2367 "-" | "/" | "%" => match (left, right) {
2368 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2369 match (l.as_str(), r.as_str()) {
2370 ("int", "int") => Some(TypeExpr::Named("int".into())),
2371 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2372 _ => None,
2373 }
2374 }
2375 _ => None,
2376 },
2377 "*" => match (left, right) {
2378 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2379 match (l.as_str(), r.as_str()) {
2380 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2381 ("int", "int") => Some(TypeExpr::Named("int".into())),
2382 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2383 _ => None,
2384 }
2385 }
2386 _ => None,
2387 },
2388 "??" => match (left, right) {
2389 (Some(TypeExpr::Union(members)), _) => {
2390 let non_nil: Vec<_> = members
2391 .iter()
2392 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2393 .cloned()
2394 .collect();
2395 if non_nil.len() == 1 {
2396 Some(non_nil[0].clone())
2397 } else if non_nil.is_empty() {
2398 right.clone()
2399 } else {
2400 Some(TypeExpr::Union(non_nil))
2401 }
2402 }
2403 _ => right.clone(),
2404 },
2405 "|>" => None,
2406 _ => None,
2407 }
2408}
2409
2410pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2415 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2416 let mut details = Vec::new();
2417 for field in ef {
2418 if field.optional {
2419 continue;
2420 }
2421 match af.iter().find(|f| f.name == field.name) {
2422 None => details.push(format!(
2423 "missing field '{}' ({})",
2424 field.name,
2425 format_type(&field.type_expr)
2426 )),
2427 Some(actual_field) => {
2428 let e_str = format_type(&field.type_expr);
2429 let a_str = format_type(&actual_field.type_expr);
2430 if e_str != a_str {
2431 details.push(format!(
2432 "field '{}' has type {}, expected {}",
2433 field.name, a_str, e_str
2434 ));
2435 }
2436 }
2437 }
2438 }
2439 if details.is_empty() {
2440 None
2441 } else {
2442 Some(details.join("; "))
2443 }
2444 } else {
2445 None
2446 }
2447}
2448
2449pub fn format_type(ty: &TypeExpr) -> String {
2450 match ty {
2451 TypeExpr::Named(n) => n.clone(),
2452 TypeExpr::Union(types) => types
2453 .iter()
2454 .map(format_type)
2455 .collect::<Vec<_>>()
2456 .join(" | "),
2457 TypeExpr::Shape(fields) => {
2458 let inner: Vec<String> = fields
2459 .iter()
2460 .map(|f| {
2461 let opt = if f.optional { "?" } else { "" };
2462 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2463 })
2464 .collect();
2465 format!("{{{}}}", inner.join(", "))
2466 }
2467 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2468 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2469 TypeExpr::FnType {
2470 params,
2471 return_type,
2472 } => {
2473 let params_str = params
2474 .iter()
2475 .map(format_type)
2476 .collect::<Vec<_>>()
2477 .join(", ");
2478 format!("fn({}) -> {}", params_str, format_type(return_type))
2479 }
2480 }
2481}
2482
2483fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2485 let remaining: Vec<TypeExpr> = members
2486 .iter()
2487 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2488 .cloned()
2489 .collect();
2490 match remaining.len() {
2491 0 => None,
2492 1 => Some(remaining.into_iter().next().unwrap()),
2493 _ => Some(TypeExpr::Union(remaining)),
2494 }
2495}
2496
2497fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2499 if members
2500 .iter()
2501 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2502 {
2503 Some(TypeExpr::Named(target.to_string()))
2504 } else {
2505 None
2506 }
2507}
2508
2509fn extract_type_of_var(node: &SNode) -> Option<String> {
2511 if let Node::FunctionCall { name, args } = &node.node {
2512 if name == "type_of" && args.len() == 1 {
2513 if let Node::Identifier(var) = &args[0].node {
2514 return Some(var.clone());
2515 }
2516 }
2517 }
2518 None
2519}
2520
2521fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
2523 for (var_name, narrowed_type) in refinements {
2524 if !scope.narrowed_vars.contains_key(var_name) {
2526 if let Some(original) = scope.get_var(var_name).cloned() {
2527 scope.narrowed_vars.insert(var_name.clone(), original);
2528 }
2529 }
2530 scope.define_var(var_name, narrowed_type.clone());
2531 }
2532}
2533
2534#[cfg(test)]
2535mod tests {
2536 use super::*;
2537 use crate::Parser;
2538 use harn_lexer::Lexer;
2539
2540 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
2541 let mut lexer = Lexer::new(source);
2542 let tokens = lexer.tokenize().unwrap();
2543 let mut parser = Parser::new(tokens);
2544 let program = parser.parse().unwrap();
2545 TypeChecker::new().check(&program)
2546 }
2547
2548 fn errors(source: &str) -> Vec<String> {
2549 check_source(source)
2550 .into_iter()
2551 .filter(|d| d.severity == DiagnosticSeverity::Error)
2552 .map(|d| d.message)
2553 .collect()
2554 }
2555
2556 #[test]
2557 fn test_no_errors_for_untyped_code() {
2558 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
2559 assert!(errs.is_empty());
2560 }
2561
2562 #[test]
2563 fn test_correct_typed_let() {
2564 let errs = errors("pipeline t(task) { let x: int = 42 }");
2565 assert!(errs.is_empty());
2566 }
2567
2568 #[test]
2569 fn test_type_mismatch_let() {
2570 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
2571 assert_eq!(errs.len(), 1);
2572 assert!(errs[0].contains("Type mismatch"));
2573 assert!(errs[0].contains("int"));
2574 assert!(errs[0].contains("string"));
2575 }
2576
2577 #[test]
2578 fn test_correct_typed_fn() {
2579 let errs = errors(
2580 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
2581 );
2582 assert!(errs.is_empty());
2583 }
2584
2585 #[test]
2586 fn test_fn_arg_type_mismatch() {
2587 let errs = errors(
2588 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
2589add("hello", 2) }"#,
2590 );
2591 assert_eq!(errs.len(), 1);
2592 assert!(errs[0].contains("Argument 1"));
2593 assert!(errs[0].contains("expected int"));
2594 }
2595
2596 #[test]
2597 fn test_return_type_mismatch() {
2598 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2599 assert_eq!(errs.len(), 1);
2600 assert!(errs[0].contains("Return type mismatch"));
2601 }
2602
2603 #[test]
2604 fn test_union_type_compatible() {
2605 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2606 assert!(errs.is_empty());
2607 }
2608
2609 #[test]
2610 fn test_union_type_mismatch() {
2611 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2612 assert_eq!(errs.len(), 1);
2613 assert!(errs[0].contains("Type mismatch"));
2614 }
2615
2616 #[test]
2617 fn test_type_inference_propagation() {
2618 let errs = errors(
2619 r#"pipeline t(task) {
2620 fn add(a: int, b: int) -> int { return a + b }
2621 let result: string = add(1, 2)
2622}"#,
2623 );
2624 assert_eq!(errs.len(), 1);
2625 assert!(errs[0].contains("Type mismatch"));
2626 assert!(errs[0].contains("string"));
2627 assert!(errs[0].contains("int"));
2628 }
2629
2630 #[test]
2631 fn test_builtin_return_type_inference() {
2632 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2633 assert_eq!(errs.len(), 1);
2634 assert!(errs[0].contains("string"));
2635 assert!(errs[0].contains("int"));
2636 }
2637
2638 #[test]
2639 fn test_workflow_and_transcript_builtins_are_known() {
2640 let errs = errors(
2641 r#"pipeline t(task) {
2642 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2643 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2644 let run: dict = workflow_execute("task", flow, [], {})
2645 let tree: dict = load_run_tree("run.json")
2646 let fixture: dict = run_record_fixture(run?.run)
2647 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
2648 let diff: dict = run_record_diff(run?.run, run?.run)
2649 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
2650 let suite_report: dict = eval_suite_run(manifest)
2651 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
2652 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
2653 let selection: dict = artifact_editor_selection("src/main.rs", "main")
2654 let verify: dict = artifact_verification_result("verify", "ok")
2655 let test_result: dict = artifact_test_result("tests", "pass")
2656 let cmd: dict = artifact_command_result("cargo test", {status: 0})
2657 let patch: dict = artifact_diff("src/main.rs", "old", "new")
2658 let git: dict = artifact_git_diff("diff --git a b")
2659 let review: dict = artifact_diff_review(patch, "review me")
2660 let decision: dict = artifact_review_decision(review, "accepted")
2661 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
2662 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
2663 let apply: dict = artifact_apply_intent(review, "apply")
2664 let transcript = transcript_reset({metadata: {source: "test"}})
2665 let visible: string = transcript_render_visible(transcript_archive(transcript))
2666 let events: list = transcript_events(transcript)
2667 let context: string = artifact_context([], {max_artifacts: 1})
2668 println(report)
2669 println(run)
2670 println(tree)
2671 println(fixture)
2672 println(suite)
2673 println(diff)
2674 println(manifest)
2675 println(suite_report)
2676 println(wf)
2677 println(snap)
2678 println(selection)
2679 println(verify)
2680 println(test_result)
2681 println(cmd)
2682 println(patch)
2683 println(git)
2684 println(review)
2685 println(decision)
2686 println(proposal)
2687 println(bundle)
2688 println(apply)
2689 println(visible)
2690 println(events)
2691 println(context)
2692}"#,
2693 );
2694 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2695 }
2696
2697 #[test]
2698 fn test_binary_op_type_inference() {
2699 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
2700 assert_eq!(errs.len(), 1);
2701 }
2702
2703 #[test]
2704 fn test_comparison_returns_bool() {
2705 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
2706 assert!(errs.is_empty());
2707 }
2708
2709 #[test]
2710 fn test_int_float_promotion() {
2711 let errs = errors("pipeline t(task) { let x: float = 42 }");
2712 assert!(errs.is_empty());
2713 }
2714
2715 #[test]
2716 fn test_untyped_code_no_errors() {
2717 let errs = errors(
2718 r#"pipeline t(task) {
2719 fn process(data) {
2720 let result = data + " processed"
2721 return result
2722 }
2723 log(process("hello"))
2724}"#,
2725 );
2726 assert!(errs.is_empty());
2727 }
2728
2729 #[test]
2730 fn test_type_alias() {
2731 let errs = errors(
2732 r#"pipeline t(task) {
2733 type Name = string
2734 let x: Name = "hello"
2735}"#,
2736 );
2737 assert!(errs.is_empty());
2738 }
2739
2740 #[test]
2741 fn test_type_alias_mismatch() {
2742 let errs = errors(
2743 r#"pipeline t(task) {
2744 type Name = string
2745 let x: Name = 42
2746}"#,
2747 );
2748 assert_eq!(errs.len(), 1);
2749 }
2750
2751 #[test]
2752 fn test_assignment_type_check() {
2753 let errs = errors(
2754 r#"pipeline t(task) {
2755 var x: int = 0
2756 x = "hello"
2757}"#,
2758 );
2759 assert_eq!(errs.len(), 1);
2760 assert!(errs[0].contains("cannot assign string"));
2761 }
2762
2763 #[test]
2764 fn test_covariance_int_to_float_in_fn() {
2765 let errs = errors(
2766 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
2767 );
2768 assert!(errs.is_empty());
2769 }
2770
2771 #[test]
2772 fn test_covariance_return_type() {
2773 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
2774 assert!(errs.is_empty());
2775 }
2776
2777 #[test]
2778 fn test_no_contravariance_float_to_int() {
2779 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
2780 assert_eq!(errs.len(), 1);
2781 }
2782
2783 fn warnings(source: &str) -> Vec<String> {
2786 check_source(source)
2787 .into_iter()
2788 .filter(|d| d.severity == DiagnosticSeverity::Warning)
2789 .map(|d| d.message)
2790 .collect()
2791 }
2792
2793 #[test]
2794 fn test_exhaustive_match_no_warning() {
2795 let warns = warnings(
2796 r#"pipeline t(task) {
2797 enum Color { Red, Green, Blue }
2798 let c = Color.Red
2799 match c.variant {
2800 "Red" -> { log("r") }
2801 "Green" -> { log("g") }
2802 "Blue" -> { log("b") }
2803 }
2804}"#,
2805 );
2806 let exhaustive_warns: Vec<_> = warns
2807 .iter()
2808 .filter(|w| w.contains("Non-exhaustive"))
2809 .collect();
2810 assert!(exhaustive_warns.is_empty());
2811 }
2812
2813 #[test]
2814 fn test_non_exhaustive_match_warning() {
2815 let warns = warnings(
2816 r#"pipeline t(task) {
2817 enum Color { Red, Green, Blue }
2818 let c = Color.Red
2819 match c.variant {
2820 "Red" -> { log("r") }
2821 "Green" -> { log("g") }
2822 }
2823}"#,
2824 );
2825 let exhaustive_warns: Vec<_> = warns
2826 .iter()
2827 .filter(|w| w.contains("Non-exhaustive"))
2828 .collect();
2829 assert_eq!(exhaustive_warns.len(), 1);
2830 assert!(exhaustive_warns[0].contains("Blue"));
2831 }
2832
2833 #[test]
2834 fn test_non_exhaustive_multiple_missing() {
2835 let warns = warnings(
2836 r#"pipeline t(task) {
2837 enum Status { Active, Inactive, Pending }
2838 let s = Status.Active
2839 match s.variant {
2840 "Active" -> { log("a") }
2841 }
2842}"#,
2843 );
2844 let exhaustive_warns: Vec<_> = warns
2845 .iter()
2846 .filter(|w| w.contains("Non-exhaustive"))
2847 .collect();
2848 assert_eq!(exhaustive_warns.len(), 1);
2849 assert!(exhaustive_warns[0].contains("Inactive"));
2850 assert!(exhaustive_warns[0].contains("Pending"));
2851 }
2852
2853 #[test]
2854 fn test_enum_construct_type_inference() {
2855 let errs = errors(
2856 r#"pipeline t(task) {
2857 enum Color { Red, Green, Blue }
2858 let c: Color = Color.Red
2859}"#,
2860 );
2861 assert!(errs.is_empty());
2862 }
2863
2864 #[test]
2867 fn test_nil_coalescing_strips_nil() {
2868 let errs = errors(
2870 r#"pipeline t(task) {
2871 let x: string | nil = nil
2872 let y: string = x ?? "default"
2873}"#,
2874 );
2875 assert!(errs.is_empty());
2876 }
2877
2878 #[test]
2879 fn test_shape_mismatch_detail_missing_field() {
2880 let errs = errors(
2881 r#"pipeline t(task) {
2882 let x: {name: string, age: int} = {name: "hello"}
2883}"#,
2884 );
2885 assert_eq!(errs.len(), 1);
2886 assert!(
2887 errs[0].contains("missing field 'age'"),
2888 "expected detail about missing field, got: {}",
2889 errs[0]
2890 );
2891 }
2892
2893 #[test]
2894 fn test_shape_mismatch_detail_wrong_type() {
2895 let errs = errors(
2896 r#"pipeline t(task) {
2897 let x: {name: string, age: int} = {name: 42, age: 10}
2898}"#,
2899 );
2900 assert_eq!(errs.len(), 1);
2901 assert!(
2902 errs[0].contains("field 'name' has type int, expected string"),
2903 "expected detail about wrong type, got: {}",
2904 errs[0]
2905 );
2906 }
2907
2908 #[test]
2911 fn test_match_pattern_string_against_int() {
2912 let warns = warnings(
2913 r#"pipeline t(task) {
2914 let x: int = 42
2915 match x {
2916 "hello" -> { log("bad") }
2917 42 -> { log("ok") }
2918 }
2919}"#,
2920 );
2921 let pattern_warns: Vec<_> = warns
2922 .iter()
2923 .filter(|w| w.contains("Match pattern type mismatch"))
2924 .collect();
2925 assert_eq!(pattern_warns.len(), 1);
2926 assert!(pattern_warns[0].contains("matching int against string literal"));
2927 }
2928
2929 #[test]
2930 fn test_match_pattern_int_against_string() {
2931 let warns = warnings(
2932 r#"pipeline t(task) {
2933 let x: string = "hello"
2934 match x {
2935 42 -> { log("bad") }
2936 "hello" -> { log("ok") }
2937 }
2938}"#,
2939 );
2940 let pattern_warns: Vec<_> = warns
2941 .iter()
2942 .filter(|w| w.contains("Match pattern type mismatch"))
2943 .collect();
2944 assert_eq!(pattern_warns.len(), 1);
2945 assert!(pattern_warns[0].contains("matching string against int literal"));
2946 }
2947
2948 #[test]
2949 fn test_match_pattern_bool_against_int() {
2950 let warns = warnings(
2951 r#"pipeline t(task) {
2952 let x: int = 42
2953 match x {
2954 true -> { log("bad") }
2955 42 -> { log("ok") }
2956 }
2957}"#,
2958 );
2959 let pattern_warns: Vec<_> = warns
2960 .iter()
2961 .filter(|w| w.contains("Match pattern type mismatch"))
2962 .collect();
2963 assert_eq!(pattern_warns.len(), 1);
2964 assert!(pattern_warns[0].contains("matching int against bool literal"));
2965 }
2966
2967 #[test]
2968 fn test_match_pattern_float_against_string() {
2969 let warns = warnings(
2970 r#"pipeline t(task) {
2971 let x: string = "hello"
2972 match x {
2973 3.14 -> { log("bad") }
2974 "hello" -> { log("ok") }
2975 }
2976}"#,
2977 );
2978 let pattern_warns: Vec<_> = warns
2979 .iter()
2980 .filter(|w| w.contains("Match pattern type mismatch"))
2981 .collect();
2982 assert_eq!(pattern_warns.len(), 1);
2983 assert!(pattern_warns[0].contains("matching string against float literal"));
2984 }
2985
2986 #[test]
2987 fn test_match_pattern_int_against_float_ok() {
2988 let warns = warnings(
2990 r#"pipeline t(task) {
2991 let x: float = 3.14
2992 match x {
2993 42 -> { log("ok") }
2994 _ -> { log("default") }
2995 }
2996}"#,
2997 );
2998 let pattern_warns: Vec<_> = warns
2999 .iter()
3000 .filter(|w| w.contains("Match pattern type mismatch"))
3001 .collect();
3002 assert!(pattern_warns.is_empty());
3003 }
3004
3005 #[test]
3006 fn test_match_pattern_float_against_int_ok() {
3007 let warns = warnings(
3009 r#"pipeline t(task) {
3010 let x: int = 42
3011 match x {
3012 3.14 -> { log("close") }
3013 _ -> { log("default") }
3014 }
3015}"#,
3016 );
3017 let pattern_warns: Vec<_> = warns
3018 .iter()
3019 .filter(|w| w.contains("Match pattern type mismatch"))
3020 .collect();
3021 assert!(pattern_warns.is_empty());
3022 }
3023
3024 #[test]
3025 fn test_match_pattern_correct_types_no_warning() {
3026 let warns = warnings(
3027 r#"pipeline t(task) {
3028 let x: int = 42
3029 match x {
3030 1 -> { log("one") }
3031 2 -> { log("two") }
3032 _ -> { log("other") }
3033 }
3034}"#,
3035 );
3036 let pattern_warns: Vec<_> = warns
3037 .iter()
3038 .filter(|w| w.contains("Match pattern type mismatch"))
3039 .collect();
3040 assert!(pattern_warns.is_empty());
3041 }
3042
3043 #[test]
3044 fn test_match_pattern_wildcard_no_warning() {
3045 let warns = warnings(
3046 r#"pipeline t(task) {
3047 let x: int = 42
3048 match x {
3049 _ -> { log("catch all") }
3050 }
3051}"#,
3052 );
3053 let pattern_warns: Vec<_> = warns
3054 .iter()
3055 .filter(|w| w.contains("Match pattern type mismatch"))
3056 .collect();
3057 assert!(pattern_warns.is_empty());
3058 }
3059
3060 #[test]
3061 fn test_match_pattern_untyped_no_warning() {
3062 let warns = warnings(
3064 r#"pipeline t(task) {
3065 let x = some_unknown_fn()
3066 match x {
3067 "hello" -> { log("string") }
3068 42 -> { log("int") }
3069 }
3070}"#,
3071 );
3072 let pattern_warns: Vec<_> = warns
3073 .iter()
3074 .filter(|w| w.contains("Match pattern type mismatch"))
3075 .collect();
3076 assert!(pattern_warns.is_empty());
3077 }
3078
3079 fn iface_warns(source: &str) -> Vec<String> {
3082 warnings(source)
3083 .into_iter()
3084 .filter(|w| w.contains("does not satisfy interface"))
3085 .collect()
3086 }
3087
3088 #[test]
3089 fn test_interface_constraint_return_type_mismatch() {
3090 let warns = iface_warns(
3091 r#"pipeline t(task) {
3092 interface Sizable {
3093 fn size(self) -> int
3094 }
3095 struct Box { width: int }
3096 impl Box {
3097 fn size(self) -> string { return "nope" }
3098 }
3099 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3100 measure(Box({width: 3}))
3101}"#,
3102 );
3103 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3104 assert!(
3105 warns[0].contains("method 'size' returns 'string', expected 'int'"),
3106 "unexpected message: {}",
3107 warns[0]
3108 );
3109 }
3110
3111 #[test]
3112 fn test_interface_constraint_param_type_mismatch() {
3113 let warns = iface_warns(
3114 r#"pipeline t(task) {
3115 interface Processor {
3116 fn process(self, x: int) -> string
3117 }
3118 struct MyProc { name: string }
3119 impl MyProc {
3120 fn process(self, x: string) -> string { return x }
3121 }
3122 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
3123 run_proc(MyProc({name: "a"}))
3124}"#,
3125 );
3126 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3127 assert!(
3128 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
3129 "unexpected message: {}",
3130 warns[0]
3131 );
3132 }
3133
3134 #[test]
3135 fn test_interface_constraint_missing_method() {
3136 let warns = iface_warns(
3137 r#"pipeline t(task) {
3138 interface Sizable {
3139 fn size(self) -> int
3140 }
3141 struct Box { width: int }
3142 impl Box {
3143 fn area(self) -> int { return self.width }
3144 }
3145 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3146 measure(Box({width: 3}))
3147}"#,
3148 );
3149 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3150 assert!(
3151 warns[0].contains("missing method 'size'"),
3152 "unexpected message: {}",
3153 warns[0]
3154 );
3155 }
3156
3157 #[test]
3158 fn test_interface_constraint_param_count_mismatch() {
3159 let warns = iface_warns(
3160 r#"pipeline t(task) {
3161 interface Doubler {
3162 fn double(self, x: int) -> int
3163 }
3164 struct Bad { v: int }
3165 impl Bad {
3166 fn double(self) -> int { return self.v * 2 }
3167 }
3168 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3169 run_double(Bad({v: 5}))
3170}"#,
3171 );
3172 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3173 assert!(
3174 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3175 "unexpected message: {}",
3176 warns[0]
3177 );
3178 }
3179
3180 #[test]
3181 fn test_interface_constraint_satisfied() {
3182 let warns = iface_warns(
3183 r#"pipeline t(task) {
3184 interface Sizable {
3185 fn size(self) -> int
3186 }
3187 struct Box { width: int, height: int }
3188 impl Box {
3189 fn size(self) -> int { return self.width * self.height }
3190 }
3191 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3192 measure(Box({width: 3, height: 4}))
3193}"#,
3194 );
3195 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3196 }
3197
3198 #[test]
3199 fn test_interface_constraint_untyped_impl_compatible() {
3200 let warns = iface_warns(
3202 r#"pipeline t(task) {
3203 interface Sizable {
3204 fn size(self) -> int
3205 }
3206 struct Box { width: int }
3207 impl Box {
3208 fn size(self) { return self.width }
3209 }
3210 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3211 measure(Box({width: 3}))
3212}"#,
3213 );
3214 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3215 }
3216
3217 #[test]
3218 fn test_interface_constraint_int_float_covariance() {
3219 let warns = iface_warns(
3221 r#"pipeline t(task) {
3222 interface Measurable {
3223 fn value(self) -> float
3224 }
3225 struct Gauge { v: int }
3226 impl Gauge {
3227 fn value(self) -> int { return self.v }
3228 }
3229 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
3230 read_val(Gauge({v: 42}))
3231}"#,
3232 );
3233 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3234 }
3235
3236 #[test]
3239 fn test_nil_narrowing_then_branch() {
3240 let errs = errors(
3242 r#"pipeline t(task) {
3243 fn greet(name: string | nil) {
3244 if name != nil {
3245 let s: string = name
3246 }
3247 }
3248}"#,
3249 );
3250 assert!(errs.is_empty(), "got: {:?}", errs);
3251 }
3252
3253 #[test]
3254 fn test_nil_narrowing_else_branch() {
3255 let errs = errors(
3257 r#"pipeline t(task) {
3258 fn check(x: string | nil) {
3259 if x != nil {
3260 let s: string = x
3261 } else {
3262 let n: nil = x
3263 }
3264 }
3265}"#,
3266 );
3267 assert!(errs.is_empty(), "got: {:?}", errs);
3268 }
3269
3270 #[test]
3271 fn test_nil_equality_narrows_both() {
3272 let errs = errors(
3274 r#"pipeline t(task) {
3275 fn check(x: string | nil) {
3276 if x == nil {
3277 let n: nil = x
3278 } else {
3279 let s: string = x
3280 }
3281 }
3282}"#,
3283 );
3284 assert!(errs.is_empty(), "got: {:?}", errs);
3285 }
3286
3287 #[test]
3288 fn test_truthiness_narrowing() {
3289 let errs = errors(
3291 r#"pipeline t(task) {
3292 fn check(x: string | nil) {
3293 if x {
3294 let s: string = x
3295 }
3296 }
3297}"#,
3298 );
3299 assert!(errs.is_empty(), "got: {:?}", errs);
3300 }
3301
3302 #[test]
3303 fn test_negation_narrowing() {
3304 let errs = errors(
3306 r#"pipeline t(task) {
3307 fn check(x: string | nil) {
3308 if !x {
3309 let n: nil = x
3310 } else {
3311 let s: string = x
3312 }
3313 }
3314}"#,
3315 );
3316 assert!(errs.is_empty(), "got: {:?}", errs);
3317 }
3318
3319 #[test]
3320 fn test_typeof_narrowing() {
3321 let errs = errors(
3323 r#"pipeline t(task) {
3324 fn check(x: string | int) {
3325 if type_of(x) == "string" {
3326 let s: string = x
3327 }
3328 }
3329}"#,
3330 );
3331 assert!(errs.is_empty(), "got: {:?}", errs);
3332 }
3333
3334 #[test]
3335 fn test_typeof_narrowing_else() {
3336 let errs = errors(
3338 r#"pipeline t(task) {
3339 fn check(x: string | int) {
3340 if type_of(x) == "string" {
3341 let s: string = x
3342 } else {
3343 let i: int = x
3344 }
3345 }
3346}"#,
3347 );
3348 assert!(errs.is_empty(), "got: {:?}", errs);
3349 }
3350
3351 #[test]
3352 fn test_typeof_neq_narrowing() {
3353 let errs = errors(
3355 r#"pipeline t(task) {
3356 fn check(x: string | int) {
3357 if type_of(x) != "string" {
3358 let i: int = x
3359 } else {
3360 let s: string = x
3361 }
3362 }
3363}"#,
3364 );
3365 assert!(errs.is_empty(), "got: {:?}", errs);
3366 }
3367
3368 #[test]
3369 fn test_and_combines_narrowing() {
3370 let errs = errors(
3372 r#"pipeline t(task) {
3373 fn check(x: string | int | nil) {
3374 if x != nil && type_of(x) == "string" {
3375 let s: string = x
3376 }
3377 }
3378}"#,
3379 );
3380 assert!(errs.is_empty(), "got: {:?}", errs);
3381 }
3382
3383 #[test]
3384 fn test_or_falsy_narrowing() {
3385 let errs = errors(
3387 r#"pipeline t(task) {
3388 fn check(x: string | nil, y: int | nil) {
3389 if x || y {
3390 // conservative: can't narrow
3391 } else {
3392 let xn: nil = x
3393 let yn: nil = y
3394 }
3395 }
3396}"#,
3397 );
3398 assert!(errs.is_empty(), "got: {:?}", errs);
3399 }
3400
3401 #[test]
3402 fn test_guard_narrows_outer_scope() {
3403 let errs = errors(
3404 r#"pipeline t(task) {
3405 fn check(x: string | nil) {
3406 guard x != nil else { return }
3407 let s: string = x
3408 }
3409}"#,
3410 );
3411 assert!(errs.is_empty(), "got: {:?}", errs);
3412 }
3413
3414 #[test]
3415 fn test_while_narrows_body() {
3416 let errs = errors(
3417 r#"pipeline t(task) {
3418 fn check(x: string | nil) {
3419 while x != nil {
3420 let s: string = x
3421 break
3422 }
3423 }
3424}"#,
3425 );
3426 assert!(errs.is_empty(), "got: {:?}", errs);
3427 }
3428
3429 #[test]
3430 fn test_early_return_narrows_after_if() {
3431 let errs = errors(
3433 r#"pipeline t(task) {
3434 fn check(x: string | nil) -> string {
3435 if x == nil {
3436 return "default"
3437 }
3438 let s: string = x
3439 return s
3440 }
3441}"#,
3442 );
3443 assert!(errs.is_empty(), "got: {:?}", errs);
3444 }
3445
3446 #[test]
3447 fn test_early_throw_narrows_after_if() {
3448 let errs = errors(
3449 r#"pipeline t(task) {
3450 fn check(x: string | nil) {
3451 if x == nil {
3452 throw "missing"
3453 }
3454 let s: string = x
3455 }
3456}"#,
3457 );
3458 assert!(errs.is_empty(), "got: {:?}", errs);
3459 }
3460
3461 #[test]
3462 fn test_no_narrowing_unknown_type() {
3463 let errs = errors(
3465 r#"pipeline t(task) {
3466 fn check(x) {
3467 if x != nil {
3468 let s: string = x
3469 }
3470 }
3471}"#,
3472 );
3473 assert!(errs.is_empty(), "got: {:?}", errs);
3476 }
3477
3478 #[test]
3479 fn test_reassignment_invalidates_narrowing() {
3480 let errs = errors(
3482 r#"pipeline t(task) {
3483 fn check(x: string | nil) {
3484 var y: string | nil = x
3485 if y != nil {
3486 let s: string = y
3487 y = nil
3488 let s2: string = y
3489 }
3490 }
3491}"#,
3492 );
3493 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3495 assert!(
3496 errs[0].contains("Type mismatch"),
3497 "expected type mismatch, got: {}",
3498 errs[0]
3499 );
3500 }
3501
3502 #[test]
3503 fn test_let_immutable_warning() {
3504 let all = check_source(
3505 r#"pipeline t(task) {
3506 let x = 42
3507 x = 43
3508}"#,
3509 );
3510 let warnings: Vec<_> = all
3511 .iter()
3512 .filter(|d| d.severity == DiagnosticSeverity::Warning)
3513 .collect();
3514 assert!(
3515 warnings.iter().any(|w| w.message.contains("immutable")),
3516 "expected immutability warning, got: {:?}",
3517 warnings
3518 );
3519 }
3520
3521 #[test]
3522 fn test_nested_narrowing() {
3523 let errs = errors(
3524 r#"pipeline t(task) {
3525 fn check(x: string | int | nil) {
3526 if x != nil {
3527 if type_of(x) == "int" {
3528 let i: int = x
3529 }
3530 }
3531 }
3532}"#,
3533 );
3534 assert!(errs.is_empty(), "got: {:?}", errs);
3535 }
3536
3537 #[test]
3538 fn test_match_narrows_arms() {
3539 let errs = errors(
3540 r#"pipeline t(task) {
3541 fn check(x: string | int) {
3542 match x {
3543 "hello" -> {
3544 let s: string = x
3545 }
3546 42 -> {
3547 let i: int = x
3548 }
3549 _ -> {}
3550 }
3551 }
3552}"#,
3553 );
3554 assert!(errs.is_empty(), "got: {:?}", errs);
3555 }
3556
3557 #[test]
3558 fn test_has_narrows_optional_field() {
3559 let errs = errors(
3560 r#"pipeline t(task) {
3561 fn check(x: {name?: string, age: int}) {
3562 if x.has("name") {
3563 let n: {name: string, age: int} = x
3564 }
3565 }
3566}"#,
3567 );
3568 assert!(errs.is_empty(), "got: {:?}", errs);
3569 }
3570
3571 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
3576 let mut lexer = Lexer::new(source);
3577 let tokens = lexer.tokenize().unwrap();
3578 let mut parser = Parser::new(tokens);
3579 let program = parser.parse().unwrap();
3580 TypeChecker::new().check_with_source(&program, source)
3581 }
3582
3583 #[test]
3584 fn test_fix_string_plus_int_literal() {
3585 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
3586 let diags = check_source_with_source(source);
3587 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3588 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3589 let fix = fixable[0].fix.as_ref().unwrap();
3590 assert_eq!(fix.len(), 1);
3591 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
3592 }
3593
3594 #[test]
3595 fn test_fix_int_plus_string_literal() {
3596 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
3597 let diags = check_source_with_source(source);
3598 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3599 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3600 let fix = fixable[0].fix.as_ref().unwrap();
3601 assert_eq!(fix[0].replacement, "\"${42}hello\"");
3602 }
3603
3604 #[test]
3605 fn test_fix_string_plus_variable() {
3606 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
3607 let diags = check_source_with_source(source);
3608 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3609 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3610 let fix = fixable[0].fix.as_ref().unwrap();
3611 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
3612 }
3613
3614 #[test]
3615 fn test_no_fix_int_plus_int() {
3616 let source = "pipeline t(task) {\n let x: int = 5\n let y: float = 3.0\n let z = x - y\n log(z)\n}";
3618 let diags = check_source_with_source(source);
3619 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3620 assert!(
3621 fixable.is_empty(),
3622 "no fix expected for numeric ops, got: {fixable:?}"
3623 );
3624 }
3625
3626 #[test]
3627 fn test_no_fix_without_source() {
3628 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
3629 let diags = check_source(source);
3630 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3631 assert!(
3632 fixable.is_empty(),
3633 "without source, no fix should be generated"
3634 );
3635 }
3636}