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