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