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 bind_type_param(
1484 param_name: &str,
1485 concrete: &TypeExpr,
1486 bindings: &mut BTreeMap<String, TypeExpr>,
1487 ) -> Result<(), String> {
1488 if let Some(existing) = bindings.get(param_name) {
1489 if existing != concrete {
1490 return Err(format!(
1491 "type parameter '{}' was inferred as both {} and {}",
1492 param_name,
1493 format_type(existing),
1494 format_type(concrete)
1495 ));
1496 }
1497 return Ok(());
1498 }
1499 bindings.insert(param_name.to_string(), concrete.clone());
1500 Ok(())
1501 }
1502
1503 fn extract_type_bindings(
1506 param_type: &TypeExpr,
1507 arg_type: &TypeExpr,
1508 type_params: &std::collections::BTreeSet<String>,
1509 bindings: &mut BTreeMap<String, TypeExpr>,
1510 ) -> Result<(), String> {
1511 match (param_type, arg_type) {
1512 (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
1513 Self::bind_type_param(param_name, concrete, bindings)
1514 }
1515 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1516 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
1517 }
1518 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1519 Self::extract_type_bindings(pk, ak, type_params, bindings)?;
1520 Self::extract_type_bindings(pv, av, type_params, bindings)
1521 }
1522 (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
1523 for param_field in param_fields {
1524 if let Some(arg_field) = arg_fields
1525 .iter()
1526 .find(|field| field.name == param_field.name)
1527 {
1528 Self::extract_type_bindings(
1529 ¶m_field.type_expr,
1530 &arg_field.type_expr,
1531 type_params,
1532 bindings,
1533 )?;
1534 }
1535 }
1536 Ok(())
1537 }
1538 (
1539 TypeExpr::FnType {
1540 params: p_params,
1541 return_type: p_ret,
1542 },
1543 TypeExpr::FnType {
1544 params: a_params,
1545 return_type: a_ret,
1546 },
1547 ) => {
1548 for (param, arg) in p_params.iter().zip(a_params.iter()) {
1549 Self::extract_type_bindings(param, arg, type_params, bindings)?;
1550 }
1551 Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
1552 }
1553 _ => Ok(()),
1554 }
1555 }
1556
1557 fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
1558 match ty {
1559 TypeExpr::Named(name) => bindings
1560 .get(name)
1561 .cloned()
1562 .unwrap_or_else(|| TypeExpr::Named(name.clone())),
1563 TypeExpr::Union(items) => TypeExpr::Union(
1564 items
1565 .iter()
1566 .map(|item| Self::apply_type_bindings(item, bindings))
1567 .collect(),
1568 ),
1569 TypeExpr::Shape(fields) => TypeExpr::Shape(
1570 fields
1571 .iter()
1572 .map(|field| ShapeField {
1573 name: field.name.clone(),
1574 type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
1575 optional: field.optional,
1576 })
1577 .collect(),
1578 ),
1579 TypeExpr::List(inner) => {
1580 TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
1581 }
1582 TypeExpr::DictType(key, value) => TypeExpr::DictType(
1583 Box::new(Self::apply_type_bindings(key, bindings)),
1584 Box::new(Self::apply_type_bindings(value, bindings)),
1585 ),
1586 TypeExpr::FnType {
1587 params,
1588 return_type,
1589 } => TypeExpr::FnType {
1590 params: params
1591 .iter()
1592 .map(|param| Self::apply_type_bindings(param, bindings))
1593 .collect(),
1594 return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
1595 },
1596 }
1597 }
1598
1599 fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
1600 let mut inferred: Option<TypeExpr> = None;
1601 for item in items {
1602 let Some(item_type) = self.infer_type(item, scope) else {
1603 return TypeExpr::Named("list".into());
1604 };
1605 inferred = Some(match inferred {
1606 None => item_type,
1607 Some(current) if current == item_type => current,
1608 Some(TypeExpr::Union(mut members)) => {
1609 if !members.contains(&item_type) {
1610 members.push(item_type);
1611 }
1612 TypeExpr::Union(members)
1613 }
1614 Some(current) => TypeExpr::Union(vec![current, item_type]),
1615 });
1616 }
1617 inferred
1618 .map(|item_type| TypeExpr::List(Box::new(item_type)))
1619 .unwrap_or_else(|| TypeExpr::Named("list".into()))
1620 }
1621
1622 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1624 match &condition.node {
1625 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1627 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1628 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1629 return nil_ref;
1630 }
1631 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1632 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1633 return typeof_ref;
1634 }
1635 Refinements::empty()
1636 }
1637
1638 Node::BinaryOp { op, left, right } if op == "&&" => {
1640 let left_ref = Self::extract_refinements(left, scope);
1641 let right_ref = Self::extract_refinements(right, scope);
1642 let mut truthy = left_ref.truthy;
1643 truthy.extend(right_ref.truthy);
1644 Refinements {
1645 truthy,
1646 falsy: vec![],
1647 }
1648 }
1649
1650 Node::BinaryOp { op, left, right } if op == "||" => {
1652 let left_ref = Self::extract_refinements(left, scope);
1653 let right_ref = Self::extract_refinements(right, scope);
1654 let mut falsy = left_ref.falsy;
1655 falsy.extend(right_ref.falsy);
1656 Refinements {
1657 truthy: vec![],
1658 falsy,
1659 }
1660 }
1661
1662 Node::UnaryOp { op, operand } if op == "!" => {
1664 Self::extract_refinements(operand, scope).inverted()
1665 }
1666
1667 Node::Identifier(name) => {
1669 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1670 if members
1671 .iter()
1672 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1673 {
1674 if let Some(narrowed) = remove_from_union(members, "nil") {
1675 return Refinements {
1676 truthy: vec![(name.clone(), Some(narrowed))],
1677 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1678 };
1679 }
1680 }
1681 }
1682 Refinements::empty()
1683 }
1684
1685 Node::MethodCall {
1687 object,
1688 method,
1689 args,
1690 } if method == "has" && args.len() == 1 => {
1691 Self::extract_has_refinements(object, args, scope)
1692 }
1693
1694 _ => Refinements::empty(),
1695 }
1696 }
1697
1698 fn extract_nil_refinements(
1700 op: &str,
1701 left: &SNode,
1702 right: &SNode,
1703 scope: &TypeScope,
1704 ) -> Refinements {
1705 let var_node = if matches!(right.node, Node::NilLiteral) {
1706 left
1707 } else if matches!(left.node, Node::NilLiteral) {
1708 right
1709 } else {
1710 return Refinements::empty();
1711 };
1712
1713 if let Node::Identifier(name) = &var_node.node {
1714 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1715 if let Some(narrowed) = remove_from_union(members, "nil") {
1716 let neq_refs = Refinements {
1717 truthy: vec![(name.clone(), Some(narrowed))],
1718 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1719 };
1720 return if op == "!=" {
1721 neq_refs
1722 } else {
1723 neq_refs.inverted()
1724 };
1725 }
1726 }
1727 }
1728 Refinements::empty()
1729 }
1730
1731 fn extract_typeof_refinements(
1733 op: &str,
1734 left: &SNode,
1735 right: &SNode,
1736 scope: &TypeScope,
1737 ) -> Refinements {
1738 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1739 (extract_type_of_var(left), &right.node)
1740 {
1741 (var, tn.clone())
1742 } else if let (Node::StringLiteral(tn), Some(var)) =
1743 (&left.node, extract_type_of_var(right))
1744 {
1745 (var, tn.clone())
1746 } else {
1747 return Refinements::empty();
1748 };
1749
1750 const KNOWN_TYPES: &[&str] = &[
1751 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1752 ];
1753 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1754 return Refinements::empty();
1755 }
1756
1757 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(&var_name) {
1758 let narrowed = narrow_to_single(members, &type_name);
1759 let remaining = remove_from_union(members, &type_name);
1760 if narrowed.is_some() || remaining.is_some() {
1761 let eq_refs = Refinements {
1762 truthy: narrowed
1763 .map(|n| vec![(var_name.clone(), Some(n))])
1764 .unwrap_or_default(),
1765 falsy: remaining
1766 .map(|r| vec![(var_name.clone(), Some(r))])
1767 .unwrap_or_default(),
1768 };
1769 return if op == "==" {
1770 eq_refs
1771 } else {
1772 eq_refs.inverted()
1773 };
1774 }
1775 }
1776 Refinements::empty()
1777 }
1778
1779 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1781 if let Node::Identifier(var_name) = &object.node {
1782 if let Node::StringLiteral(key) = &args[0].node {
1783 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1784 if fields.iter().any(|f| f.name == *key && f.optional) {
1785 let narrowed_fields: Vec<ShapeField> = fields
1786 .iter()
1787 .map(|f| {
1788 if f.name == *key {
1789 ShapeField {
1790 name: f.name.clone(),
1791 type_expr: f.type_expr.clone(),
1792 optional: false,
1793 }
1794 } else {
1795 f.clone()
1796 }
1797 })
1798 .collect();
1799 return Refinements {
1800 truthy: vec![(
1801 var_name.clone(),
1802 Some(TypeExpr::Shape(narrowed_fields)),
1803 )],
1804 falsy: vec![],
1805 };
1806 }
1807 }
1808 }
1809 }
1810 Refinements::empty()
1811 }
1812
1813 fn block_definitely_exits(stmts: &[SNode]) -> bool {
1815 stmts.iter().any(|s| match &s.node {
1816 Node::ReturnStmt { .. }
1817 | Node::ThrowStmt { .. }
1818 | Node::BreakStmt
1819 | Node::ContinueStmt => true,
1820 Node::IfElse {
1821 then_body,
1822 else_body: Some(else_body),
1823 ..
1824 } => Self::block_definitely_exits(then_body) && Self::block_definitely_exits(else_body),
1825 _ => false,
1826 })
1827 }
1828
1829 fn check_match_exhaustiveness(
1830 &mut self,
1831 value: &SNode,
1832 arms: &[MatchArm],
1833 scope: &TypeScope,
1834 span: Span,
1835 ) {
1836 let enum_name = match &value.node {
1838 Node::PropertyAccess { object, property } if property == "variant" => {
1839 match self.infer_type(object, scope) {
1841 Some(TypeExpr::Named(name)) => {
1842 if scope.get_enum(&name).is_some() {
1843 Some(name)
1844 } else {
1845 None
1846 }
1847 }
1848 _ => None,
1849 }
1850 }
1851 _ => {
1852 match self.infer_type(value, scope) {
1854 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1855 _ => None,
1856 }
1857 }
1858 };
1859
1860 let Some(enum_name) = enum_name else {
1861 self.check_match_exhaustiveness_union(value, arms, scope, span);
1863 return;
1864 };
1865 let Some(variants) = scope.get_enum(&enum_name) else {
1866 return;
1867 };
1868
1869 let mut covered: Vec<String> = Vec::new();
1871 let mut has_wildcard = false;
1872
1873 for arm in arms {
1874 match &arm.pattern.node {
1875 Node::StringLiteral(s) => covered.push(s.clone()),
1877 Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1879 has_wildcard = true;
1880 }
1881 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1883 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1885 _ => {
1886 has_wildcard = true;
1888 }
1889 }
1890 }
1891
1892 if has_wildcard {
1893 return;
1894 }
1895
1896 let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1897 if !missing.is_empty() {
1898 let missing_str = missing
1899 .iter()
1900 .map(|s| format!("\"{}\"", s))
1901 .collect::<Vec<_>>()
1902 .join(", ");
1903 self.warning_at(
1904 format!(
1905 "Non-exhaustive match on enum {}: missing variants {}",
1906 enum_name, missing_str
1907 ),
1908 span,
1909 );
1910 }
1911 }
1912
1913 fn check_match_exhaustiveness_union(
1915 &mut self,
1916 value: &SNode,
1917 arms: &[MatchArm],
1918 scope: &TypeScope,
1919 span: Span,
1920 ) {
1921 let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
1922 return;
1923 };
1924 if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
1926 return;
1927 }
1928
1929 let mut has_wildcard = false;
1930 let mut covered_types: Vec<String> = Vec::new();
1931
1932 for arm in arms {
1933 match &arm.pattern.node {
1934 Node::NilLiteral => covered_types.push("nil".into()),
1937 Node::BoolLiteral(_) => {
1938 if !covered_types.contains(&"bool".into()) {
1939 covered_types.push("bool".into());
1940 }
1941 }
1942 Node::IntLiteral(_) => {
1943 if !covered_types.contains(&"int".into()) {
1944 covered_types.push("int".into());
1945 }
1946 }
1947 Node::FloatLiteral(_) => {
1948 if !covered_types.contains(&"float".into()) {
1949 covered_types.push("float".into());
1950 }
1951 }
1952 Node::StringLiteral(_) => {
1953 if !covered_types.contains(&"string".into()) {
1954 covered_types.push("string".into());
1955 }
1956 }
1957 Node::Identifier(name) if name == "_" => {
1958 has_wildcard = true;
1959 }
1960 _ => {
1961 has_wildcard = true;
1962 }
1963 }
1964 }
1965
1966 if has_wildcard {
1967 return;
1968 }
1969
1970 let type_names: Vec<&str> = members
1971 .iter()
1972 .filter_map(|m| match m {
1973 TypeExpr::Named(n) => Some(n.as_str()),
1974 _ => None,
1975 })
1976 .collect();
1977 let missing: Vec<&&str> = type_names
1978 .iter()
1979 .filter(|t| !covered_types.iter().any(|c| c == **t))
1980 .collect();
1981 if !missing.is_empty() {
1982 let missing_str = missing
1983 .iter()
1984 .map(|s| s.to_string())
1985 .collect::<Vec<_>>()
1986 .join(", ");
1987 self.warning_at(
1988 format!(
1989 "Non-exhaustive match on union type: missing {}",
1990 missing_str
1991 ),
1992 span,
1993 );
1994 }
1995 }
1996
1997 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1998 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
2000 if let Some(sig) = scope.get_fn(name).cloned() {
2001 if !has_spread
2002 && !is_builtin(name)
2003 && !sig.has_rest
2004 && (args.len() < sig.required_params || args.len() > sig.params.len())
2005 {
2006 let expected = if sig.required_params == sig.params.len() {
2007 format!("{}", sig.params.len())
2008 } else {
2009 format!("{}-{}", sig.required_params, sig.params.len())
2010 };
2011 self.warning_at(
2012 format!(
2013 "Function '{}' expects {} arguments, got {}",
2014 name,
2015 expected,
2016 args.len()
2017 ),
2018 span,
2019 );
2020 }
2021 let call_scope = if sig.type_param_names.is_empty() {
2024 scope.clone()
2025 } else {
2026 let mut s = scope.child();
2027 for tp_name in &sig.type_param_names {
2028 s.generic_type_params.insert(tp_name.clone());
2029 }
2030 s
2031 };
2032 let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
2033 let type_param_set: std::collections::BTreeSet<String> =
2034 sig.type_param_names.iter().cloned().collect();
2035 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2036 if let Some(param_ty) = param_type {
2037 if let Some(arg_ty) = self.infer_type(arg, scope) {
2038 if let Err(message) = Self::extract_type_bindings(
2039 param_ty,
2040 &arg_ty,
2041 &type_param_set,
2042 &mut type_bindings,
2043 ) {
2044 self.error_at(message, arg.span);
2045 }
2046 }
2047 }
2048 }
2049 for (i, (arg, (param_name, param_type))) in
2050 args.iter().zip(sig.params.iter()).enumerate()
2051 {
2052 if let Some(expected) = param_type {
2053 let actual = self.infer_type(arg, scope);
2054 if let Some(actual) = &actual {
2055 let expected = Self::apply_type_bindings(expected, &type_bindings);
2056 if !self.types_compatible(&expected, actual, &call_scope) {
2057 self.error_at(
2058 format!(
2059 "Argument {} ('{}'): expected {}, got {}",
2060 i + 1,
2061 param_name,
2062 format_type(&expected),
2063 format_type(actual)
2064 ),
2065 arg.span,
2066 );
2067 }
2068 }
2069 }
2070 }
2071 if !sig.where_clauses.is_empty() {
2072 for (type_param, bound) in &sig.where_clauses {
2073 if let Some(concrete_type) = type_bindings.get(type_param) {
2074 let concrete_name = format_type(concrete_type);
2075 if let Some(reason) =
2076 self.interface_mismatch_reason(&concrete_name, bound, scope)
2077 {
2078 self.error_at(
2079 format!(
2080 "Type '{}' does not satisfy interface '{}': {} \
2081 (required by constraint `where {}: {}`)",
2082 concrete_name, bound, reason, type_param, bound
2083 ),
2084 span,
2085 );
2086 }
2087 }
2088 }
2089 }
2090 }
2091 for arg in args {
2093 self.check_node(arg, scope);
2094 }
2095 }
2096
2097 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
2099 match &snode.node {
2100 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
2101 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
2102 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
2103 Some(TypeExpr::Named("string".into()))
2104 }
2105 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
2106 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
2107 Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
2108 Node::DictLiteral(entries) => {
2109 let mut fields = Vec::new();
2111 for entry in entries {
2112 let key = match &entry.key.node {
2113 Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
2114 _ => return Some(TypeExpr::Named("dict".into())),
2115 };
2116 let val_type = self
2117 .infer_type(&entry.value, scope)
2118 .unwrap_or(TypeExpr::Named("nil".into()));
2119 fields.push(ShapeField {
2120 name: key,
2121 type_expr: val_type,
2122 optional: false,
2123 });
2124 }
2125 if !fields.is_empty() {
2126 Some(TypeExpr::Shape(fields))
2127 } else {
2128 Some(TypeExpr::Named("dict".into()))
2129 }
2130 }
2131 Node::Closure { params, body, .. } => {
2132 let all_typed = params.iter().all(|p| p.type_expr.is_some());
2134 if all_typed && !params.is_empty() {
2135 let param_types: Vec<TypeExpr> =
2136 params.iter().filter_map(|p| p.type_expr.clone()).collect();
2137 let ret = body.last().and_then(|last| self.infer_type(last, scope));
2139 if let Some(ret_type) = ret {
2140 return Some(TypeExpr::FnType {
2141 params: param_types,
2142 return_type: Box::new(ret_type),
2143 });
2144 }
2145 }
2146 Some(TypeExpr::Named("closure".into()))
2147 }
2148
2149 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
2150
2151 Node::FunctionCall { name, args } => {
2152 if scope.get_struct(name).is_some() {
2154 return Some(TypeExpr::Named(name.clone()));
2155 }
2156 if let Some(sig) = scope.get_fn(name) {
2158 let mut return_type = sig.return_type.clone();
2159 if let Some(ty) = return_type.take() {
2160 if sig.type_param_names.is_empty() {
2161 return Some(ty);
2162 }
2163 let mut bindings = BTreeMap::new();
2164 let type_param_set: std::collections::BTreeSet<String> =
2165 sig.type_param_names.iter().cloned().collect();
2166 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2167 if let Some(param_ty) = param_type {
2168 if let Some(arg_ty) = self.infer_type(arg, scope) {
2169 let _ = Self::extract_type_bindings(
2170 param_ty,
2171 &arg_ty,
2172 &type_param_set,
2173 &mut bindings,
2174 );
2175 }
2176 }
2177 }
2178 return Some(Self::apply_type_bindings(&ty, &bindings));
2179 }
2180 return None;
2181 }
2182 builtin_return_type(name)
2184 }
2185
2186 Node::BinaryOp { op, left, right } => {
2187 let lt = self.infer_type(left, scope);
2188 let rt = self.infer_type(right, scope);
2189 infer_binary_op_type(op, <, &rt)
2190 }
2191
2192 Node::UnaryOp { op, operand } => {
2193 let t = self.infer_type(operand, scope);
2194 match op.as_str() {
2195 "!" => Some(TypeExpr::Named("bool".into())),
2196 "-" => t, _ => None,
2198 }
2199 }
2200
2201 Node::Ternary {
2202 condition,
2203 true_expr,
2204 false_expr,
2205 } => {
2206 let refs = Self::extract_refinements(condition, scope);
2207
2208 let mut true_scope = scope.child();
2209 apply_refinements(&mut true_scope, &refs.truthy);
2210 let tt = self.infer_type(true_expr, &true_scope);
2211
2212 let mut false_scope = scope.child();
2213 apply_refinements(&mut false_scope, &refs.falsy);
2214 let ft = self.infer_type(false_expr, &false_scope);
2215
2216 match (&tt, &ft) {
2217 (Some(a), Some(b)) if a == b => tt,
2218 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
2219 (Some(_), None) => tt,
2220 (None, Some(_)) => ft,
2221 (None, None) => None,
2222 }
2223 }
2224
2225 Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
2226
2227 Node::PropertyAccess { object, property } => {
2228 if let Node::Identifier(name) = &object.node {
2230 if scope.get_enum(name).is_some() {
2231 return Some(TypeExpr::Named(name.clone()));
2232 }
2233 }
2234 if property == "variant" {
2236 let obj_type = self.infer_type(object, scope);
2237 if let Some(TypeExpr::Named(name)) = &obj_type {
2238 if scope.get_enum(name).is_some() {
2239 return Some(TypeExpr::Named("string".into()));
2240 }
2241 }
2242 }
2243 let obj_type = self.infer_type(object, scope);
2245 if let Some(TypeExpr::Shape(fields)) = &obj_type {
2246 if let Some(field) = fields.iter().find(|f| f.name == *property) {
2247 return Some(field.type_expr.clone());
2248 }
2249 }
2250 None
2251 }
2252
2253 Node::SubscriptAccess { object, index } => {
2254 let obj_type = self.infer_type(object, scope);
2255 match &obj_type {
2256 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
2257 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
2258 Some(TypeExpr::Shape(fields)) => {
2259 if let Node::StringLiteral(key) = &index.node {
2261 fields
2262 .iter()
2263 .find(|f| &f.name == key)
2264 .map(|f| f.type_expr.clone())
2265 } else {
2266 None
2267 }
2268 }
2269 Some(TypeExpr::Named(n)) if n == "list" => None,
2270 Some(TypeExpr::Named(n)) if n == "dict" => None,
2271 Some(TypeExpr::Named(n)) if n == "string" => {
2272 Some(TypeExpr::Named("string".into()))
2273 }
2274 _ => None,
2275 }
2276 }
2277 Node::SliceAccess { object, .. } => {
2278 let obj_type = self.infer_type(object, scope);
2280 match &obj_type {
2281 Some(TypeExpr::List(_)) => obj_type,
2282 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
2283 Some(TypeExpr::Named(n)) if n == "string" => {
2284 Some(TypeExpr::Named("string".into()))
2285 }
2286 _ => None,
2287 }
2288 }
2289 Node::MethodCall { object, method, .. }
2290 | Node::OptionalMethodCall { object, method, .. } => {
2291 let obj_type = self.infer_type(object, scope);
2292 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
2293 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
2294 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
2295 match method.as_str() {
2296 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
2298 Some(TypeExpr::Named("bool".into()))
2299 }
2300 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
2302 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
2304 | "pad_left" | "pad_right" | "repeat" | "join" => {
2305 Some(TypeExpr::Named("string".into()))
2306 }
2307 "split" | "chars" => Some(TypeExpr::Named("list".into())),
2308 "filter" => {
2310 if is_dict {
2311 Some(TypeExpr::Named("dict".into()))
2312 } else {
2313 Some(TypeExpr::Named("list".into()))
2314 }
2315 }
2316 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2318 "reduce" | "find" | "first" | "last" => None,
2319 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2321 "merge" | "map_values" | "rekey" | "map_keys" => {
2322 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2326 Some(TypeExpr::DictType(
2327 Box::new(TypeExpr::Named("string".into())),
2328 v.clone(),
2329 ))
2330 } else {
2331 Some(TypeExpr::Named("dict".into()))
2332 }
2333 }
2334 "to_string" => Some(TypeExpr::Named("string".into())),
2336 "to_int" => Some(TypeExpr::Named("int".into())),
2337 "to_float" => Some(TypeExpr::Named("float".into())),
2338 _ => None,
2339 }
2340 }
2341
2342 Node::TryOperator { operand } => {
2344 match self.infer_type(operand, scope) {
2345 Some(TypeExpr::Named(name)) if name == "Result" => None, _ => None,
2347 }
2348 }
2349
2350 _ => None,
2351 }
2352 }
2353
2354 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2356 if let TypeExpr::Named(name) = expected {
2358 if scope.is_generic_type_param(name) {
2359 return true;
2360 }
2361 }
2362 if let TypeExpr::Named(name) = actual {
2363 if scope.is_generic_type_param(name) {
2364 return true;
2365 }
2366 }
2367 let expected = self.resolve_alias(expected, scope);
2368 let actual = self.resolve_alias(actual, scope);
2369
2370 if let TypeExpr::Named(iface_name) = &expected {
2373 if scope.get_interface(iface_name).is_some() {
2374 if let TypeExpr::Named(type_name) = &actual {
2375 return self.satisfies_interface(type_name, iface_name, scope);
2376 }
2377 return false;
2378 }
2379 }
2380
2381 match (&expected, &actual) {
2382 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2383 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2386 act_members.iter().all(|am| {
2387 exp_members
2388 .iter()
2389 .any(|em| self.types_compatible(em, am, scope))
2390 })
2391 }
2392 (TypeExpr::Union(members), actual_type) => members
2393 .iter()
2394 .any(|m| self.types_compatible(m, actual_type, scope)),
2395 (expected_type, TypeExpr::Union(members)) => members
2396 .iter()
2397 .all(|m| self.types_compatible(expected_type, m, scope)),
2398 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2399 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2400 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2401 if expected_field.optional {
2402 return true;
2403 }
2404 af.iter().any(|actual_field| {
2405 actual_field.name == expected_field.name
2406 && self.types_compatible(
2407 &expected_field.type_expr,
2408 &actual_field.type_expr,
2409 scope,
2410 )
2411 })
2412 }),
2413 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2415 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2416 keys_ok
2417 && af
2418 .iter()
2419 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2420 }
2421 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2423 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2424 self.types_compatible(expected_inner, actual_inner, scope)
2425 }
2426 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2427 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2428 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2429 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2430 }
2431 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2432 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2433 (
2435 TypeExpr::FnType {
2436 params: ep,
2437 return_type: er,
2438 },
2439 TypeExpr::FnType {
2440 params: ap,
2441 return_type: ar,
2442 },
2443 ) => {
2444 ep.len() == ap.len()
2445 && ep
2446 .iter()
2447 .zip(ap.iter())
2448 .all(|(e, a)| self.types_compatible(e, a, scope))
2449 && self.types_compatible(er, ar, scope)
2450 }
2451 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2453 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2454 _ => false,
2455 }
2456 }
2457
2458 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2459 if let TypeExpr::Named(name) = ty {
2460 if let Some(resolved) = scope.resolve_type(name) {
2461 return resolved.clone();
2462 }
2463 }
2464 ty.clone()
2465 }
2466
2467 fn error_at(&mut self, message: String, span: Span) {
2468 self.diagnostics.push(TypeDiagnostic {
2469 message,
2470 severity: DiagnosticSeverity::Error,
2471 span: Some(span),
2472 help: None,
2473 fix: None,
2474 });
2475 }
2476
2477 #[allow(dead_code)]
2478 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2479 self.diagnostics.push(TypeDiagnostic {
2480 message,
2481 severity: DiagnosticSeverity::Error,
2482 span: Some(span),
2483 help: Some(help),
2484 fix: None,
2485 });
2486 }
2487
2488 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
2489 self.diagnostics.push(TypeDiagnostic {
2490 message,
2491 severity: DiagnosticSeverity::Error,
2492 span: Some(span),
2493 help: None,
2494 fix: Some(fix),
2495 });
2496 }
2497
2498 fn warning_at(&mut self, message: String, span: Span) {
2499 self.diagnostics.push(TypeDiagnostic {
2500 message,
2501 severity: DiagnosticSeverity::Warning,
2502 span: Some(span),
2503 help: None,
2504 fix: None,
2505 });
2506 }
2507
2508 #[allow(dead_code)]
2509 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2510 self.diagnostics.push(TypeDiagnostic {
2511 message,
2512 severity: DiagnosticSeverity::Warning,
2513 span: Some(span),
2514 help: Some(help),
2515 fix: None,
2516 });
2517 }
2518
2519 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
2523 match &snode.node {
2524 Node::BinaryOp { op, left, right } => {
2525 self.check_binops(left, scope);
2526 self.check_binops(right, scope);
2527 let lt = self.infer_type(left, scope);
2528 let rt = self.infer_type(right, scope);
2529 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
2530 let span = snode.span;
2531 match op.as_str() {
2532 "+" => {
2533 let valid = matches!(
2534 (l.as_str(), r.as_str()),
2535 ("int" | "float", "int" | "float")
2536 | ("string", "string")
2537 | ("list", "list")
2538 | ("dict", "dict")
2539 );
2540 if !valid {
2541 let msg =
2542 format!("Operator '+' is not valid for types {} and {}", l, r);
2543 let fix = if l == "string" || r == "string" {
2544 self.build_interpolation_fix(left, right, l == "string", span)
2545 } else {
2546 None
2547 };
2548 if let Some(fix) = fix {
2549 self.error_at_with_fix(msg, span, fix);
2550 } else {
2551 self.error_at(msg, span);
2552 }
2553 }
2554 }
2555 "-" | "/" | "%" => {
2556 let numeric = ["int", "float"];
2557 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
2558 self.error_at(
2559 format!(
2560 "Operator '{}' requires numeric operands, got {} and {}",
2561 op, l, r
2562 ),
2563 span,
2564 );
2565 }
2566 }
2567 "*" => {
2568 let numeric = ["int", "float"];
2569 let is_numeric =
2570 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
2571 let is_string_repeat =
2572 (l == "string" && r == "int") || (l == "int" && r == "string");
2573 if !is_numeric && !is_string_repeat {
2574 self.error_at(
2575 format!(
2576 "Operator '*' requires numeric operands or string * int, got {} and {}",
2577 l, r
2578 ),
2579 span,
2580 );
2581 }
2582 }
2583 _ => {}
2584 }
2585 }
2586 }
2587 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
2589 _ => {}
2590 }
2591 }
2592
2593 fn build_interpolation_fix(
2595 &self,
2596 left: &SNode,
2597 right: &SNode,
2598 left_is_string: bool,
2599 expr_span: Span,
2600 ) -> Option<Vec<FixEdit>> {
2601 let src = self.source.as_ref()?;
2602 let (str_node, other_node) = if left_is_string {
2603 (left, right)
2604 } else {
2605 (right, left)
2606 };
2607 let str_text = src.get(str_node.span.start..str_node.span.end)?;
2608 let other_text = src.get(other_node.span.start..other_node.span.end)?;
2609 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
2611 if other_text.contains('}') || other_text.contains('"') {
2613 return None;
2614 }
2615 let replacement = if left_is_string {
2616 format!("\"{inner}${{{other_text}}}\"")
2617 } else {
2618 format!("\"${{{other_text}}}{inner}\"")
2619 };
2620 Some(vec![FixEdit {
2621 span: expr_span,
2622 replacement,
2623 }])
2624 }
2625}
2626
2627impl Default for TypeChecker {
2628 fn default() -> Self {
2629 Self::new()
2630 }
2631}
2632
2633fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2635 match op {
2636 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2637 Some(TypeExpr::Named("bool".into()))
2638 }
2639 "+" => match (left, right) {
2640 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2641 match (l.as_str(), r.as_str()) {
2642 ("int", "int") => Some(TypeExpr::Named("int".into())),
2643 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2644 ("string", "string") => Some(TypeExpr::Named("string".into())),
2645 ("list", "list") => Some(TypeExpr::Named("list".into())),
2646 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2647 _ => None,
2648 }
2649 }
2650 _ => None,
2651 },
2652 "-" | "/" | "%" => match (left, right) {
2653 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2654 match (l.as_str(), r.as_str()) {
2655 ("int", "int") => Some(TypeExpr::Named("int".into())),
2656 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2657 _ => None,
2658 }
2659 }
2660 _ => None,
2661 },
2662 "*" => match (left, right) {
2663 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2664 match (l.as_str(), r.as_str()) {
2665 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2666 ("int", "int") => Some(TypeExpr::Named("int".into())),
2667 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2668 _ => None,
2669 }
2670 }
2671 _ => None,
2672 },
2673 "??" => match (left, right) {
2674 (Some(TypeExpr::Union(members)), _) => {
2676 let non_nil: Vec<_> = members
2677 .iter()
2678 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2679 .cloned()
2680 .collect();
2681 if non_nil.len() == 1 {
2682 Some(non_nil[0].clone())
2683 } else if non_nil.is_empty() {
2684 right.clone()
2685 } else {
2686 Some(TypeExpr::Union(non_nil))
2687 }
2688 }
2689 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
2691 (Some(l), _) => Some(l.clone()),
2693 (None, _) => right.clone(),
2695 },
2696 "|>" => None,
2697 _ => None,
2698 }
2699}
2700
2701pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2706 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2707 let mut details = Vec::new();
2708 for field in ef {
2709 if field.optional {
2710 continue;
2711 }
2712 match af.iter().find(|f| f.name == field.name) {
2713 None => details.push(format!(
2714 "missing field '{}' ({})",
2715 field.name,
2716 format_type(&field.type_expr)
2717 )),
2718 Some(actual_field) => {
2719 let e_str = format_type(&field.type_expr);
2720 let a_str = format_type(&actual_field.type_expr);
2721 if e_str != a_str {
2722 details.push(format!(
2723 "field '{}' has type {}, expected {}",
2724 field.name, a_str, e_str
2725 ));
2726 }
2727 }
2728 }
2729 }
2730 if details.is_empty() {
2731 None
2732 } else {
2733 Some(details.join("; "))
2734 }
2735 } else {
2736 None
2737 }
2738}
2739
2740fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
2743 matches!(
2744 &value.node,
2745 Node::IntLiteral(_)
2746 | Node::FloatLiteral(_)
2747 | Node::StringLiteral(_)
2748 | Node::BoolLiteral(_)
2749 | Node::NilLiteral
2750 | Node::ListLiteral(_)
2751 | Node::DictLiteral(_)
2752 | Node::InterpolatedString(_)
2753 )
2754}
2755
2756pub fn format_type(ty: &TypeExpr) -> String {
2757 match ty {
2758 TypeExpr::Named(n) => n.clone(),
2759 TypeExpr::Union(types) => types
2760 .iter()
2761 .map(format_type)
2762 .collect::<Vec<_>>()
2763 .join(" | "),
2764 TypeExpr::Shape(fields) => {
2765 let inner: Vec<String> = fields
2766 .iter()
2767 .map(|f| {
2768 let opt = if f.optional { "?" } else { "" };
2769 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2770 })
2771 .collect();
2772 format!("{{{}}}", inner.join(", "))
2773 }
2774 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2775 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2776 TypeExpr::FnType {
2777 params,
2778 return_type,
2779 } => {
2780 let params_str = params
2781 .iter()
2782 .map(format_type)
2783 .collect::<Vec<_>>()
2784 .join(", ");
2785 format!("fn({}) -> {}", params_str, format_type(return_type))
2786 }
2787 }
2788}
2789
2790fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2792 let remaining: Vec<TypeExpr> = members
2793 .iter()
2794 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2795 .cloned()
2796 .collect();
2797 match remaining.len() {
2798 0 => None,
2799 1 => Some(remaining.into_iter().next().unwrap()),
2800 _ => Some(TypeExpr::Union(remaining)),
2801 }
2802}
2803
2804fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2806 if members
2807 .iter()
2808 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2809 {
2810 Some(TypeExpr::Named(target.to_string()))
2811 } else {
2812 None
2813 }
2814}
2815
2816fn extract_type_of_var(node: &SNode) -> Option<String> {
2818 if let Node::FunctionCall { name, args } = &node.node {
2819 if name == "type_of" && args.len() == 1 {
2820 if let Node::Identifier(var) = &args[0].node {
2821 return Some(var.clone());
2822 }
2823 }
2824 }
2825 None
2826}
2827
2828fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
2830 for (var_name, narrowed_type) in refinements {
2831 if !scope.narrowed_vars.contains_key(var_name) {
2833 if let Some(original) = scope.get_var(var_name).cloned() {
2834 scope.narrowed_vars.insert(var_name.clone(), original);
2835 }
2836 }
2837 scope.define_var(var_name, narrowed_type.clone());
2838 }
2839}
2840
2841#[cfg(test)]
2842mod tests {
2843 use super::*;
2844 use crate::Parser;
2845 use harn_lexer::Lexer;
2846
2847 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
2848 let mut lexer = Lexer::new(source);
2849 let tokens = lexer.tokenize().unwrap();
2850 let mut parser = Parser::new(tokens);
2851 let program = parser.parse().unwrap();
2852 TypeChecker::new().check(&program)
2853 }
2854
2855 fn errors(source: &str) -> Vec<String> {
2856 check_source(source)
2857 .into_iter()
2858 .filter(|d| d.severity == DiagnosticSeverity::Error)
2859 .map(|d| d.message)
2860 .collect()
2861 }
2862
2863 #[test]
2864 fn test_no_errors_for_untyped_code() {
2865 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
2866 assert!(errs.is_empty());
2867 }
2868
2869 #[test]
2870 fn test_correct_typed_let() {
2871 let errs = errors("pipeline t(task) { let x: int = 42 }");
2872 assert!(errs.is_empty());
2873 }
2874
2875 #[test]
2876 fn test_type_mismatch_let() {
2877 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
2878 assert_eq!(errs.len(), 1);
2879 assert!(errs[0].contains("Type mismatch"));
2880 assert!(errs[0].contains("int"));
2881 assert!(errs[0].contains("string"));
2882 }
2883
2884 #[test]
2885 fn test_correct_typed_fn() {
2886 let errs = errors(
2887 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
2888 );
2889 assert!(errs.is_empty());
2890 }
2891
2892 #[test]
2893 fn test_fn_arg_type_mismatch() {
2894 let errs = errors(
2895 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
2896add("hello", 2) }"#,
2897 );
2898 assert_eq!(errs.len(), 1);
2899 assert!(errs[0].contains("Argument 1"));
2900 assert!(errs[0].contains("expected int"));
2901 }
2902
2903 #[test]
2904 fn test_return_type_mismatch() {
2905 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2906 assert_eq!(errs.len(), 1);
2907 assert!(errs[0].contains("Return type mismatch"));
2908 }
2909
2910 #[test]
2911 fn test_union_type_compatible() {
2912 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2913 assert!(errs.is_empty());
2914 }
2915
2916 #[test]
2917 fn test_union_type_mismatch() {
2918 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2919 assert_eq!(errs.len(), 1);
2920 assert!(errs[0].contains("Type mismatch"));
2921 }
2922
2923 #[test]
2924 fn test_type_inference_propagation() {
2925 let errs = errors(
2926 r#"pipeline t(task) {
2927 fn add(a: int, b: int) -> int { return a + b }
2928 let result: string = add(1, 2)
2929}"#,
2930 );
2931 assert_eq!(errs.len(), 1);
2932 assert!(errs[0].contains("Type mismatch"));
2933 assert!(errs[0].contains("string"));
2934 assert!(errs[0].contains("int"));
2935 }
2936
2937 #[test]
2938 fn test_generic_return_type_instantiates_from_callsite() {
2939 let errs = errors(
2940 r#"pipeline t(task) {
2941 fn identity<T>(x: T) -> T { return x }
2942 fn first<T>(items: list<T>) -> T { return items[0] }
2943 let n: int = identity(42)
2944 let s: string = first(["a", "b"])
2945}"#,
2946 );
2947 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2948 }
2949
2950 #[test]
2951 fn test_generic_type_param_must_bind_consistently() {
2952 let errs = errors(
2953 r#"pipeline t(task) {
2954 fn keep<T>(a: T, b: T) -> T { return a }
2955 keep(1, "x")
2956}"#,
2957 );
2958 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
2959 assert!(
2960 errs.iter()
2961 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
2962 "missing generic binding conflict error: {:?}",
2963 errs
2964 );
2965 assert!(
2966 errs.iter()
2967 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
2968 "missing instantiated argument mismatch error: {:?}",
2969 errs
2970 );
2971 }
2972
2973 #[test]
2974 fn test_generic_list_binding_propagates_element_type() {
2975 let errs = errors(
2976 r#"pipeline t(task) {
2977 fn first<T>(items: list<T>) -> T { return items[0] }
2978 let bad: string = first([1, 2, 3])
2979}"#,
2980 );
2981 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
2982 assert!(errs[0].contains("declared as string, but assigned int"));
2983 }
2984
2985 #[test]
2986 fn test_builtin_return_type_inference() {
2987 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2988 assert_eq!(errs.len(), 1);
2989 assert!(errs[0].contains("string"));
2990 assert!(errs[0].contains("int"));
2991 }
2992
2993 #[test]
2994 fn test_workflow_and_transcript_builtins_are_known() {
2995 let errs = errors(
2996 r#"pipeline t(task) {
2997 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2998 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2999 let run: dict = workflow_execute("task", flow, [], {})
3000 let tree: dict = load_run_tree("run.json")
3001 let fixture: dict = run_record_fixture(run?.run)
3002 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
3003 let diff: dict = run_record_diff(run?.run, run?.run)
3004 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
3005 let suite_report: dict = eval_suite_run(manifest)
3006 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
3007 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
3008 let selection: dict = artifact_editor_selection("src/main.rs", "main")
3009 let verify: dict = artifact_verification_result("verify", "ok")
3010 let test_result: dict = artifact_test_result("tests", "pass")
3011 let cmd: dict = artifact_command_result("cargo test", {status: 0})
3012 let patch: dict = artifact_diff("src/main.rs", "old", "new")
3013 let git: dict = artifact_git_diff("diff --git a b")
3014 let review: dict = artifact_diff_review(patch, "review me")
3015 let decision: dict = artifact_review_decision(review, "accepted")
3016 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
3017 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
3018 let apply: dict = artifact_apply_intent(review, "apply")
3019 let transcript = transcript_reset({metadata: {source: "test"}})
3020 let visible: string = transcript_render_visible(transcript_archive(transcript))
3021 let events: list = transcript_events(transcript)
3022 let context: string = artifact_context([], {max_artifacts: 1})
3023 println(report)
3024 println(run)
3025 println(tree)
3026 println(fixture)
3027 println(suite)
3028 println(diff)
3029 println(manifest)
3030 println(suite_report)
3031 println(wf)
3032 println(snap)
3033 println(selection)
3034 println(verify)
3035 println(test_result)
3036 println(cmd)
3037 println(patch)
3038 println(git)
3039 println(review)
3040 println(decision)
3041 println(proposal)
3042 println(bundle)
3043 println(apply)
3044 println(visible)
3045 println(events)
3046 println(context)
3047}"#,
3048 );
3049 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
3050 }
3051
3052 #[test]
3053 fn test_binary_op_type_inference() {
3054 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
3055 assert_eq!(errs.len(), 1);
3056 }
3057
3058 #[test]
3059 fn test_comparison_returns_bool() {
3060 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
3061 assert!(errs.is_empty());
3062 }
3063
3064 #[test]
3065 fn test_int_float_promotion() {
3066 let errs = errors("pipeline t(task) { let x: float = 42 }");
3067 assert!(errs.is_empty());
3068 }
3069
3070 #[test]
3071 fn test_untyped_code_no_errors() {
3072 let errs = errors(
3073 r#"pipeline t(task) {
3074 fn process(data) {
3075 let result = data + " processed"
3076 return result
3077 }
3078 log(process("hello"))
3079}"#,
3080 );
3081 assert!(errs.is_empty());
3082 }
3083
3084 #[test]
3085 fn test_type_alias() {
3086 let errs = errors(
3087 r#"pipeline t(task) {
3088 type Name = string
3089 let x: Name = "hello"
3090}"#,
3091 );
3092 assert!(errs.is_empty());
3093 }
3094
3095 #[test]
3096 fn test_type_alias_mismatch() {
3097 let errs = errors(
3098 r#"pipeline t(task) {
3099 type Name = string
3100 let x: Name = 42
3101}"#,
3102 );
3103 assert_eq!(errs.len(), 1);
3104 }
3105
3106 #[test]
3107 fn test_assignment_type_check() {
3108 let errs = errors(
3109 r#"pipeline t(task) {
3110 var x: int = 0
3111 x = "hello"
3112}"#,
3113 );
3114 assert_eq!(errs.len(), 1);
3115 assert!(errs[0].contains("cannot assign string"));
3116 }
3117
3118 #[test]
3119 fn test_covariance_int_to_float_in_fn() {
3120 let errs = errors(
3121 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
3122 );
3123 assert!(errs.is_empty());
3124 }
3125
3126 #[test]
3127 fn test_covariance_return_type() {
3128 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
3129 assert!(errs.is_empty());
3130 }
3131
3132 #[test]
3133 fn test_no_contravariance_float_to_int() {
3134 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
3135 assert_eq!(errs.len(), 1);
3136 }
3137
3138 fn warnings(source: &str) -> Vec<String> {
3141 check_source(source)
3142 .into_iter()
3143 .filter(|d| d.severity == DiagnosticSeverity::Warning)
3144 .map(|d| d.message)
3145 .collect()
3146 }
3147
3148 #[test]
3149 fn test_exhaustive_match_no_warning() {
3150 let warns = warnings(
3151 r#"pipeline t(task) {
3152 enum Color { Red, Green, Blue }
3153 let c = Color.Red
3154 match c.variant {
3155 "Red" -> { log("r") }
3156 "Green" -> { log("g") }
3157 "Blue" -> { log("b") }
3158 }
3159}"#,
3160 );
3161 let exhaustive_warns: Vec<_> = warns
3162 .iter()
3163 .filter(|w| w.contains("Non-exhaustive"))
3164 .collect();
3165 assert!(exhaustive_warns.is_empty());
3166 }
3167
3168 #[test]
3169 fn test_non_exhaustive_match_warning() {
3170 let warns = warnings(
3171 r#"pipeline t(task) {
3172 enum Color { Red, Green, Blue }
3173 let c = Color.Red
3174 match c.variant {
3175 "Red" -> { log("r") }
3176 "Green" -> { log("g") }
3177 }
3178}"#,
3179 );
3180 let exhaustive_warns: Vec<_> = warns
3181 .iter()
3182 .filter(|w| w.contains("Non-exhaustive"))
3183 .collect();
3184 assert_eq!(exhaustive_warns.len(), 1);
3185 assert!(exhaustive_warns[0].contains("Blue"));
3186 }
3187
3188 #[test]
3189 fn test_non_exhaustive_multiple_missing() {
3190 let warns = warnings(
3191 r#"pipeline t(task) {
3192 enum Status { Active, Inactive, Pending }
3193 let s = Status.Active
3194 match s.variant {
3195 "Active" -> { log("a") }
3196 }
3197}"#,
3198 );
3199 let exhaustive_warns: Vec<_> = warns
3200 .iter()
3201 .filter(|w| w.contains("Non-exhaustive"))
3202 .collect();
3203 assert_eq!(exhaustive_warns.len(), 1);
3204 assert!(exhaustive_warns[0].contains("Inactive"));
3205 assert!(exhaustive_warns[0].contains("Pending"));
3206 }
3207
3208 #[test]
3209 fn test_enum_construct_type_inference() {
3210 let errs = errors(
3211 r#"pipeline t(task) {
3212 enum Color { Red, Green, Blue }
3213 let c: Color = Color.Red
3214}"#,
3215 );
3216 assert!(errs.is_empty());
3217 }
3218
3219 #[test]
3222 fn test_nil_coalescing_strips_nil() {
3223 let errs = errors(
3225 r#"pipeline t(task) {
3226 let x: string | nil = nil
3227 let y: string = x ?? "default"
3228}"#,
3229 );
3230 assert!(errs.is_empty());
3231 }
3232
3233 #[test]
3234 fn test_shape_mismatch_detail_missing_field() {
3235 let errs = errors(
3236 r#"pipeline t(task) {
3237 let x: {name: string, age: int} = {name: "hello"}
3238}"#,
3239 );
3240 assert_eq!(errs.len(), 1);
3241 assert!(
3242 errs[0].contains("missing field 'age'"),
3243 "expected detail about missing field, got: {}",
3244 errs[0]
3245 );
3246 }
3247
3248 #[test]
3249 fn test_shape_mismatch_detail_wrong_type() {
3250 let errs = errors(
3251 r#"pipeline t(task) {
3252 let x: {name: string, age: int} = {name: 42, age: 10}
3253}"#,
3254 );
3255 assert_eq!(errs.len(), 1);
3256 assert!(
3257 errs[0].contains("field 'name' has type int, expected string"),
3258 "expected detail about wrong type, got: {}",
3259 errs[0]
3260 );
3261 }
3262
3263 #[test]
3266 fn test_match_pattern_string_against_int() {
3267 let warns = warnings(
3268 r#"pipeline t(task) {
3269 let x: int = 42
3270 match x {
3271 "hello" -> { log("bad") }
3272 42 -> { log("ok") }
3273 }
3274}"#,
3275 );
3276 let pattern_warns: Vec<_> = warns
3277 .iter()
3278 .filter(|w| w.contains("Match pattern type mismatch"))
3279 .collect();
3280 assert_eq!(pattern_warns.len(), 1);
3281 assert!(pattern_warns[0].contains("matching int against string literal"));
3282 }
3283
3284 #[test]
3285 fn test_match_pattern_int_against_string() {
3286 let warns = warnings(
3287 r#"pipeline t(task) {
3288 let x: string = "hello"
3289 match x {
3290 42 -> { log("bad") }
3291 "hello" -> { log("ok") }
3292 }
3293}"#,
3294 );
3295 let pattern_warns: Vec<_> = warns
3296 .iter()
3297 .filter(|w| w.contains("Match pattern type mismatch"))
3298 .collect();
3299 assert_eq!(pattern_warns.len(), 1);
3300 assert!(pattern_warns[0].contains("matching string against int literal"));
3301 }
3302
3303 #[test]
3304 fn test_match_pattern_bool_against_int() {
3305 let warns = warnings(
3306 r#"pipeline t(task) {
3307 let x: int = 42
3308 match x {
3309 true -> { log("bad") }
3310 42 -> { log("ok") }
3311 }
3312}"#,
3313 );
3314 let pattern_warns: Vec<_> = warns
3315 .iter()
3316 .filter(|w| w.contains("Match pattern type mismatch"))
3317 .collect();
3318 assert_eq!(pattern_warns.len(), 1);
3319 assert!(pattern_warns[0].contains("matching int against bool literal"));
3320 }
3321
3322 #[test]
3323 fn test_match_pattern_float_against_string() {
3324 let warns = warnings(
3325 r#"pipeline t(task) {
3326 let x: string = "hello"
3327 match x {
3328 3.14 -> { log("bad") }
3329 "hello" -> { log("ok") }
3330 }
3331}"#,
3332 );
3333 let pattern_warns: Vec<_> = warns
3334 .iter()
3335 .filter(|w| w.contains("Match pattern type mismatch"))
3336 .collect();
3337 assert_eq!(pattern_warns.len(), 1);
3338 assert!(pattern_warns[0].contains("matching string against float literal"));
3339 }
3340
3341 #[test]
3342 fn test_match_pattern_int_against_float_ok() {
3343 let warns = warnings(
3345 r#"pipeline t(task) {
3346 let x: float = 3.14
3347 match x {
3348 42 -> { log("ok") }
3349 _ -> { log("default") }
3350 }
3351}"#,
3352 );
3353 let pattern_warns: Vec<_> = warns
3354 .iter()
3355 .filter(|w| w.contains("Match pattern type mismatch"))
3356 .collect();
3357 assert!(pattern_warns.is_empty());
3358 }
3359
3360 #[test]
3361 fn test_match_pattern_float_against_int_ok() {
3362 let warns = warnings(
3364 r#"pipeline t(task) {
3365 let x: int = 42
3366 match x {
3367 3.14 -> { log("close") }
3368 _ -> { log("default") }
3369 }
3370}"#,
3371 );
3372 let pattern_warns: Vec<_> = warns
3373 .iter()
3374 .filter(|w| w.contains("Match pattern type mismatch"))
3375 .collect();
3376 assert!(pattern_warns.is_empty());
3377 }
3378
3379 #[test]
3380 fn test_match_pattern_correct_types_no_warning() {
3381 let warns = warnings(
3382 r#"pipeline t(task) {
3383 let x: int = 42
3384 match x {
3385 1 -> { log("one") }
3386 2 -> { log("two") }
3387 _ -> { log("other") }
3388 }
3389}"#,
3390 );
3391 let pattern_warns: Vec<_> = warns
3392 .iter()
3393 .filter(|w| w.contains("Match pattern type mismatch"))
3394 .collect();
3395 assert!(pattern_warns.is_empty());
3396 }
3397
3398 #[test]
3399 fn test_match_pattern_wildcard_no_warning() {
3400 let warns = warnings(
3401 r#"pipeline t(task) {
3402 let x: int = 42
3403 match x {
3404 _ -> { log("catch all") }
3405 }
3406}"#,
3407 );
3408 let pattern_warns: Vec<_> = warns
3409 .iter()
3410 .filter(|w| w.contains("Match pattern type mismatch"))
3411 .collect();
3412 assert!(pattern_warns.is_empty());
3413 }
3414
3415 #[test]
3416 fn test_match_pattern_untyped_no_warning() {
3417 let warns = warnings(
3419 r#"pipeline t(task) {
3420 let x = some_unknown_fn()
3421 match x {
3422 "hello" -> { log("string") }
3423 42 -> { log("int") }
3424 }
3425}"#,
3426 );
3427 let pattern_warns: Vec<_> = warns
3428 .iter()
3429 .filter(|w| w.contains("Match pattern type mismatch"))
3430 .collect();
3431 assert!(pattern_warns.is_empty());
3432 }
3433
3434 fn iface_errors(source: &str) -> Vec<String> {
3437 errors(source)
3438 .into_iter()
3439 .filter(|message| message.contains("does not satisfy interface"))
3440 .collect()
3441 }
3442
3443 #[test]
3444 fn test_interface_constraint_return_type_mismatch() {
3445 let warns = iface_errors(
3446 r#"pipeline t(task) {
3447 interface Sizable {
3448 fn size(self) -> int
3449 }
3450 struct Box { width: int }
3451 impl Box {
3452 fn size(self) -> string { return "nope" }
3453 }
3454 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3455 measure(Box({width: 3}))
3456}"#,
3457 );
3458 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3459 assert!(
3460 warns[0].contains("method 'size' returns 'string', expected 'int'"),
3461 "unexpected message: {}",
3462 warns[0]
3463 );
3464 }
3465
3466 #[test]
3467 fn test_interface_constraint_param_type_mismatch() {
3468 let warns = iface_errors(
3469 r#"pipeline t(task) {
3470 interface Processor {
3471 fn process(self, x: int) -> string
3472 }
3473 struct MyProc { name: string }
3474 impl MyProc {
3475 fn process(self, x: string) -> string { return x }
3476 }
3477 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
3478 run_proc(MyProc({name: "a"}))
3479}"#,
3480 );
3481 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3482 assert!(
3483 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
3484 "unexpected message: {}",
3485 warns[0]
3486 );
3487 }
3488
3489 #[test]
3490 fn test_interface_constraint_missing_method() {
3491 let warns = iface_errors(
3492 r#"pipeline t(task) {
3493 interface Sizable {
3494 fn size(self) -> int
3495 }
3496 struct Box { width: int }
3497 impl Box {
3498 fn area(self) -> int { return self.width }
3499 }
3500 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3501 measure(Box({width: 3}))
3502}"#,
3503 );
3504 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3505 assert!(
3506 warns[0].contains("missing method 'size'"),
3507 "unexpected message: {}",
3508 warns[0]
3509 );
3510 }
3511
3512 #[test]
3513 fn test_interface_constraint_param_count_mismatch() {
3514 let warns = iface_errors(
3515 r#"pipeline t(task) {
3516 interface Doubler {
3517 fn double(self, x: int) -> int
3518 }
3519 struct Bad { v: int }
3520 impl Bad {
3521 fn double(self) -> int { return self.v * 2 }
3522 }
3523 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3524 run_double(Bad({v: 5}))
3525}"#,
3526 );
3527 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3528 assert!(
3529 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3530 "unexpected message: {}",
3531 warns[0]
3532 );
3533 }
3534
3535 #[test]
3536 fn test_interface_constraint_satisfied() {
3537 let warns = iface_errors(
3538 r#"pipeline t(task) {
3539 interface Sizable {
3540 fn size(self) -> int
3541 }
3542 struct Box { width: int, height: int }
3543 impl Box {
3544 fn size(self) -> int { return self.width * self.height }
3545 }
3546 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3547 measure(Box({width: 3, height: 4}))
3548}"#,
3549 );
3550 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3551 }
3552
3553 #[test]
3554 fn test_interface_constraint_untyped_impl_compatible() {
3555 let warns = iface_errors(
3557 r#"pipeline t(task) {
3558 interface Sizable {
3559 fn size(self) -> int
3560 }
3561 struct Box { width: int }
3562 impl Box {
3563 fn size(self) { return self.width }
3564 }
3565 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3566 measure(Box({width: 3}))
3567}"#,
3568 );
3569 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3570 }
3571
3572 #[test]
3573 fn test_interface_constraint_int_float_covariance() {
3574 let warns = iface_errors(
3576 r#"pipeline t(task) {
3577 interface Measurable {
3578 fn value(self) -> float
3579 }
3580 struct Gauge { v: int }
3581 impl Gauge {
3582 fn value(self) -> int { return self.v }
3583 }
3584 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
3585 read_val(Gauge({v: 42}))
3586}"#,
3587 );
3588 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3589 }
3590
3591 #[test]
3594 fn test_nil_narrowing_then_branch() {
3595 let errs = errors(
3597 r#"pipeline t(task) {
3598 fn greet(name: string | nil) {
3599 if name != nil {
3600 let s: string = name
3601 }
3602 }
3603}"#,
3604 );
3605 assert!(errs.is_empty(), "got: {:?}", errs);
3606 }
3607
3608 #[test]
3609 fn test_nil_narrowing_else_branch() {
3610 let errs = errors(
3612 r#"pipeline t(task) {
3613 fn check(x: string | nil) {
3614 if x != nil {
3615 let s: string = x
3616 } else {
3617 let n: nil = x
3618 }
3619 }
3620}"#,
3621 );
3622 assert!(errs.is_empty(), "got: {:?}", errs);
3623 }
3624
3625 #[test]
3626 fn test_nil_equality_narrows_both() {
3627 let errs = errors(
3629 r#"pipeline t(task) {
3630 fn check(x: string | nil) {
3631 if x == nil {
3632 let n: nil = x
3633 } else {
3634 let s: string = x
3635 }
3636 }
3637}"#,
3638 );
3639 assert!(errs.is_empty(), "got: {:?}", errs);
3640 }
3641
3642 #[test]
3643 fn test_truthiness_narrowing() {
3644 let errs = errors(
3646 r#"pipeline t(task) {
3647 fn check(x: string | nil) {
3648 if x {
3649 let s: string = x
3650 }
3651 }
3652}"#,
3653 );
3654 assert!(errs.is_empty(), "got: {:?}", errs);
3655 }
3656
3657 #[test]
3658 fn test_negation_narrowing() {
3659 let errs = errors(
3661 r#"pipeline t(task) {
3662 fn check(x: string | nil) {
3663 if !x {
3664 let n: nil = x
3665 } else {
3666 let s: string = x
3667 }
3668 }
3669}"#,
3670 );
3671 assert!(errs.is_empty(), "got: {:?}", errs);
3672 }
3673
3674 #[test]
3675 fn test_typeof_narrowing() {
3676 let errs = errors(
3678 r#"pipeline t(task) {
3679 fn check(x: string | int) {
3680 if type_of(x) == "string" {
3681 let s: string = x
3682 }
3683 }
3684}"#,
3685 );
3686 assert!(errs.is_empty(), "got: {:?}", errs);
3687 }
3688
3689 #[test]
3690 fn test_typeof_narrowing_else() {
3691 let errs = errors(
3693 r#"pipeline t(task) {
3694 fn check(x: string | int) {
3695 if type_of(x) == "string" {
3696 let s: string = x
3697 } else {
3698 let i: int = x
3699 }
3700 }
3701}"#,
3702 );
3703 assert!(errs.is_empty(), "got: {:?}", errs);
3704 }
3705
3706 #[test]
3707 fn test_typeof_neq_narrowing() {
3708 let errs = errors(
3710 r#"pipeline t(task) {
3711 fn check(x: string | int) {
3712 if type_of(x) != "string" {
3713 let i: int = x
3714 } else {
3715 let s: string = x
3716 }
3717 }
3718}"#,
3719 );
3720 assert!(errs.is_empty(), "got: {:?}", errs);
3721 }
3722
3723 #[test]
3724 fn test_and_combines_narrowing() {
3725 let errs = errors(
3727 r#"pipeline t(task) {
3728 fn check(x: string | int | nil) {
3729 if x != nil && type_of(x) == "string" {
3730 let s: string = x
3731 }
3732 }
3733}"#,
3734 );
3735 assert!(errs.is_empty(), "got: {:?}", errs);
3736 }
3737
3738 #[test]
3739 fn test_or_falsy_narrowing() {
3740 let errs = errors(
3742 r#"pipeline t(task) {
3743 fn check(x: string | nil, y: int | nil) {
3744 if x || y {
3745 // conservative: can't narrow
3746 } else {
3747 let xn: nil = x
3748 let yn: nil = y
3749 }
3750 }
3751}"#,
3752 );
3753 assert!(errs.is_empty(), "got: {:?}", errs);
3754 }
3755
3756 #[test]
3757 fn test_guard_narrows_outer_scope() {
3758 let errs = errors(
3759 r#"pipeline t(task) {
3760 fn check(x: string | nil) {
3761 guard x != nil else { return }
3762 let s: string = x
3763 }
3764}"#,
3765 );
3766 assert!(errs.is_empty(), "got: {:?}", errs);
3767 }
3768
3769 #[test]
3770 fn test_while_narrows_body() {
3771 let errs = errors(
3772 r#"pipeline t(task) {
3773 fn check(x: string | nil) {
3774 while x != nil {
3775 let s: string = x
3776 break
3777 }
3778 }
3779}"#,
3780 );
3781 assert!(errs.is_empty(), "got: {:?}", errs);
3782 }
3783
3784 #[test]
3785 fn test_early_return_narrows_after_if() {
3786 let errs = errors(
3788 r#"pipeline t(task) {
3789 fn check(x: string | nil) -> string {
3790 if x == nil {
3791 return "default"
3792 }
3793 let s: string = x
3794 return s
3795 }
3796}"#,
3797 );
3798 assert!(errs.is_empty(), "got: {:?}", errs);
3799 }
3800
3801 #[test]
3802 fn test_early_throw_narrows_after_if() {
3803 let errs = errors(
3804 r#"pipeline t(task) {
3805 fn check(x: string | nil) {
3806 if x == nil {
3807 throw "missing"
3808 }
3809 let s: string = x
3810 }
3811}"#,
3812 );
3813 assert!(errs.is_empty(), "got: {:?}", errs);
3814 }
3815
3816 #[test]
3817 fn test_no_narrowing_unknown_type() {
3818 let errs = errors(
3820 r#"pipeline t(task) {
3821 fn check(x) {
3822 if x != nil {
3823 let s: string = x
3824 }
3825 }
3826}"#,
3827 );
3828 assert!(errs.is_empty(), "got: {:?}", errs);
3831 }
3832
3833 #[test]
3834 fn test_reassignment_invalidates_narrowing() {
3835 let errs = errors(
3837 r#"pipeline t(task) {
3838 fn check(x: string | nil) {
3839 var y: string | nil = x
3840 if y != nil {
3841 let s: string = y
3842 y = nil
3843 let s2: string = y
3844 }
3845 }
3846}"#,
3847 );
3848 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3850 assert!(
3851 errs[0].contains("Type mismatch"),
3852 "expected type mismatch, got: {}",
3853 errs[0]
3854 );
3855 }
3856
3857 #[test]
3858 fn test_let_immutable_warning() {
3859 let all = check_source(
3860 r#"pipeline t(task) {
3861 let x = 42
3862 x = 43
3863}"#,
3864 );
3865 let warnings: Vec<_> = all
3866 .iter()
3867 .filter(|d| d.severity == DiagnosticSeverity::Warning)
3868 .collect();
3869 assert!(
3870 warnings.iter().any(|w| w.message.contains("immutable")),
3871 "expected immutability warning, got: {:?}",
3872 warnings
3873 );
3874 }
3875
3876 #[test]
3877 fn test_nested_narrowing() {
3878 let errs = errors(
3879 r#"pipeline t(task) {
3880 fn check(x: string | int | nil) {
3881 if x != nil {
3882 if type_of(x) == "int" {
3883 let i: int = x
3884 }
3885 }
3886 }
3887}"#,
3888 );
3889 assert!(errs.is_empty(), "got: {:?}", errs);
3890 }
3891
3892 #[test]
3893 fn test_match_narrows_arms() {
3894 let errs = errors(
3895 r#"pipeline t(task) {
3896 fn check(x: string | int) {
3897 match x {
3898 "hello" -> {
3899 let s: string = x
3900 }
3901 42 -> {
3902 let i: int = x
3903 }
3904 _ -> {}
3905 }
3906 }
3907}"#,
3908 );
3909 assert!(errs.is_empty(), "got: {:?}", errs);
3910 }
3911
3912 #[test]
3913 fn test_has_narrows_optional_field() {
3914 let errs = errors(
3915 r#"pipeline t(task) {
3916 fn check(x: {name?: string, age: int}) {
3917 if x.has("name") {
3918 let n: {name: string, age: int} = x
3919 }
3920 }
3921}"#,
3922 );
3923 assert!(errs.is_empty(), "got: {:?}", errs);
3924 }
3925
3926 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
3931 let mut lexer = Lexer::new(source);
3932 let tokens = lexer.tokenize().unwrap();
3933 let mut parser = Parser::new(tokens);
3934 let program = parser.parse().unwrap();
3935 TypeChecker::new().check_with_source(&program, source)
3936 }
3937
3938 #[test]
3939 fn test_fix_string_plus_int_literal() {
3940 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
3941 let diags = check_source_with_source(source);
3942 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3943 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3944 let fix = fixable[0].fix.as_ref().unwrap();
3945 assert_eq!(fix.len(), 1);
3946 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
3947 }
3948
3949 #[test]
3950 fn test_fix_int_plus_string_literal() {
3951 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
3952 let diags = check_source_with_source(source);
3953 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3954 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3955 let fix = fixable[0].fix.as_ref().unwrap();
3956 assert_eq!(fix[0].replacement, "\"${42}hello\"");
3957 }
3958
3959 #[test]
3960 fn test_fix_string_plus_variable() {
3961 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
3962 let diags = check_source_with_source(source);
3963 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3964 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3965 let fix = fixable[0].fix.as_ref().unwrap();
3966 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
3967 }
3968
3969 #[test]
3970 fn test_no_fix_int_plus_int() {
3971 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}";
3973 let diags = check_source_with_source(source);
3974 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3975 assert!(
3976 fixable.is_empty(),
3977 "no fix expected for numeric ops, got: {fixable:?}"
3978 );
3979 }
3980
3981 #[test]
3982 fn test_no_fix_without_source() {
3983 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
3984 let diags = check_source(source);
3985 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3986 assert!(
3987 fixable.is_empty(),
3988 "without source, no fix should be generated"
3989 );
3990 }
3991
3992 #[test]
3995 fn test_union_exhaustive_match_no_warning() {
3996 let warns = warnings(
3997 r#"pipeline t(task) {
3998 let x: string | int | nil = nil
3999 match x {
4000 "hello" -> { log("s") }
4001 42 -> { log("i") }
4002 nil -> { log("n") }
4003 }
4004}"#,
4005 );
4006 let union_warns: Vec<_> = warns
4007 .iter()
4008 .filter(|w| w.contains("Non-exhaustive match on union"))
4009 .collect();
4010 assert!(union_warns.is_empty());
4011 }
4012
4013 #[test]
4014 fn test_union_non_exhaustive_match_warning() {
4015 let warns = warnings(
4016 r#"pipeline t(task) {
4017 let x: string | int | nil = nil
4018 match x {
4019 "hello" -> { log("s") }
4020 42 -> { log("i") }
4021 }
4022}"#,
4023 );
4024 let union_warns: Vec<_> = warns
4025 .iter()
4026 .filter(|w| w.contains("Non-exhaustive match on union"))
4027 .collect();
4028 assert_eq!(union_warns.len(), 1);
4029 assert!(union_warns[0].contains("nil"));
4030 }
4031
4032 #[test]
4035 fn test_nil_coalesce_non_union_preserves_left_type() {
4036 let errs = errors(
4038 r#"pipeline t(task) {
4039 let x: int = 42
4040 let y: int = x ?? 0
4041}"#,
4042 );
4043 assert!(errs.is_empty());
4044 }
4045
4046 #[test]
4047 fn test_nil_coalesce_nil_returns_right_type() {
4048 let errs = errors(
4049 r#"pipeline t(task) {
4050 let x: string = nil ?? "fallback"
4051}"#,
4052 );
4053 assert!(errs.is_empty());
4054 }
4055}