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