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.error_at(
829 format!(
830 "Operator '{}' requires numeric operands, got {} and {}",
831 op, 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.error_at(
845 format!(
846 "Operator '*' requires numeric operands or string * int, got {} and {}",
847 l, r
848 ),
849 span,
850 );
851 }
852 }
853 "+" => {
854 let valid = matches!(
855 (l.as_str(), r.as_str()),
856 ("int" | "float", "int" | "float")
857 | ("string", "string")
858 | ("list", "list")
859 | ("dict", "dict")
860 );
861 if !valid {
862 self.error_at(
863 format!("Operator '+' is not valid for types {} and {}", l, r),
864 span,
865 );
866 }
867 }
868 "<" | ">" | "<=" | ">=" => {
869 let comparable = ["int", "float", "string"];
870 if !comparable.contains(&l.as_str())
871 || !comparable.contains(&r.as_str())
872 {
873 self.warning_at(
874 format!(
875 "Comparison '{}' may not be meaningful for types {} and {}",
876 op, l, r
877 ),
878 span,
879 );
880 } else if (l == "string") != (r == "string") {
881 self.warning_at(
882 format!(
883 "Comparing {} with {} using '{}' may give unexpected results",
884 l, r, op
885 ),
886 span,
887 );
888 }
889 }
890 _ => {}
891 }
892 }
893 }
894 Node::UnaryOp { operand, .. } => {
895 self.check_node(operand, scope);
896 }
897 Node::MethodCall {
898 object,
899 method,
900 args,
901 ..
902 }
903 | Node::OptionalMethodCall {
904 object,
905 method,
906 args,
907 ..
908 } => {
909 self.check_node(object, scope);
910 for arg in args {
911 self.check_node(arg, scope);
912 }
913 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
917 if scope.is_generic_type_param(&type_name) {
918 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
919 if let Some(iface_methods) = scope.get_interface(iface_name) {
920 let has_method = iface_methods.iter().any(|m| m.name == *method);
921 if !has_method {
922 self.warning_at(
923 format!(
924 "Method '{}' not found in interface '{}' (constraint on '{}')",
925 method, iface_name, type_name
926 ),
927 span,
928 );
929 }
930 }
931 }
932 }
933 }
934 }
935 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
936 self.check_node(object, scope);
937 }
938 Node::SubscriptAccess { object, index } => {
939 self.check_node(object, scope);
940 self.check_node(index, scope);
941 }
942 Node::SliceAccess { object, start, end } => {
943 self.check_node(object, scope);
944 if let Some(s) = start {
945 self.check_node(s, scope);
946 }
947 if let Some(e) = end {
948 self.check_node(e, scope);
949 }
950 }
951
952 Node::Ternary {
954 condition,
955 true_expr,
956 false_expr,
957 } => {
958 self.check_node(condition, scope);
959 let refs = Self::extract_refinements(condition, scope);
960
961 let mut true_scope = scope.child();
962 apply_refinements(&mut true_scope, &refs.truthy);
963 self.check_node(true_expr, &mut true_scope);
964
965 let mut false_scope = scope.child();
966 apply_refinements(&mut false_scope, &refs.falsy);
967 self.check_node(false_expr, &mut false_scope);
968 }
969
970 Node::ThrowStmt { value } => {
971 self.check_node(value, scope);
972 }
973
974 Node::GuardStmt {
975 condition,
976 else_body,
977 } => {
978 self.check_node(condition, scope);
979 let refs = Self::extract_refinements(condition, scope);
980
981 let mut else_scope = scope.child();
982 apply_refinements(&mut else_scope, &refs.falsy);
983 self.check_block(else_body, &mut else_scope);
984
985 apply_refinements(scope, &refs.truthy);
988 }
989
990 Node::SpawnExpr { body } => {
991 let mut spawn_scope = scope.child();
992 self.check_block(body, &mut spawn_scope);
993 }
994
995 Node::Parallel {
996 count,
997 variable,
998 body,
999 } => {
1000 self.check_node(count, scope);
1001 let mut par_scope = scope.child();
1002 if let Some(var) = variable {
1003 par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
1004 }
1005 self.check_block(body, &mut par_scope);
1006 }
1007
1008 Node::ParallelMap {
1009 list,
1010 variable,
1011 body,
1012 }
1013 | Node::ParallelSettle {
1014 list,
1015 variable,
1016 body,
1017 } => {
1018 self.check_node(list, scope);
1019 let mut par_scope = scope.child();
1020 let elem_type = match self.infer_type(list, scope) {
1021 Some(TypeExpr::List(inner)) => Some(*inner),
1022 _ => None,
1023 };
1024 par_scope.define_var(variable, elem_type);
1025 self.check_block(body, &mut par_scope);
1026 }
1027
1028 Node::SelectExpr {
1029 cases,
1030 timeout,
1031 default_body,
1032 } => {
1033 for case in cases {
1034 self.check_node(&case.channel, scope);
1035 let mut case_scope = scope.child();
1036 case_scope.define_var(&case.variable, None);
1037 self.check_block(&case.body, &mut case_scope);
1038 }
1039 if let Some((dur, body)) = timeout {
1040 self.check_node(dur, scope);
1041 let mut timeout_scope = scope.child();
1042 self.check_block(body, &mut timeout_scope);
1043 }
1044 if let Some(body) = default_body {
1045 let mut default_scope = scope.child();
1046 self.check_block(body, &mut default_scope);
1047 }
1048 }
1049
1050 Node::DeadlineBlock { duration, body } => {
1051 self.check_node(duration, scope);
1052 let mut block_scope = scope.child();
1053 self.check_block(body, &mut block_scope);
1054 }
1055
1056 Node::MutexBlock { body } => {
1057 let mut block_scope = scope.child();
1058 self.check_block(body, &mut block_scope);
1059 }
1060
1061 Node::Retry { count, body } => {
1062 self.check_node(count, scope);
1063 let mut retry_scope = scope.child();
1064 self.check_block(body, &mut retry_scope);
1065 }
1066
1067 Node::Closure { params, body, .. } => {
1068 let mut closure_scope = scope.child();
1069 for p in params {
1070 closure_scope.define_var(&p.name, p.type_expr.clone());
1071 }
1072 self.check_block(body, &mut closure_scope);
1073 }
1074
1075 Node::ListLiteral(elements) => {
1076 for elem in elements {
1077 self.check_node(elem, scope);
1078 }
1079 }
1080
1081 Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
1082 for entry in entries {
1083 self.check_node(&entry.key, scope);
1084 self.check_node(&entry.value, scope);
1085 }
1086 }
1087
1088 Node::RangeExpr { start, end, .. } => {
1089 self.check_node(start, scope);
1090 self.check_node(end, scope);
1091 }
1092
1093 Node::Spread(inner) => {
1094 self.check_node(inner, scope);
1095 }
1096
1097 Node::Block(stmts) => {
1098 let mut block_scope = scope.child();
1099 self.check_block(stmts, &mut block_scope);
1100 }
1101
1102 Node::YieldExpr { value } => {
1103 if let Some(v) = value {
1104 self.check_node(v, scope);
1105 }
1106 }
1107
1108 Node::StructConstruct {
1110 struct_name,
1111 fields,
1112 } => {
1113 for entry in fields {
1114 self.check_node(&entry.key, scope);
1115 self.check_node(&entry.value, scope);
1116 }
1117 if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
1118 for entry in fields {
1120 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1121 if !declared_fields.iter().any(|(name, _)| name == key) {
1122 self.warning_at(
1123 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1124 entry.key.span,
1125 );
1126 }
1127 }
1128 }
1129 let provided: Vec<String> = fields
1131 .iter()
1132 .filter_map(|e| match &e.key.node {
1133 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1134 _ => None,
1135 })
1136 .collect();
1137 for (name, _) in &declared_fields {
1138 if !provided.contains(name) {
1139 self.warning_at(
1140 format!(
1141 "Missing field '{}' in struct '{}' construction",
1142 name, struct_name
1143 ),
1144 span,
1145 );
1146 }
1147 }
1148 }
1149 }
1150
1151 Node::EnumConstruct {
1153 enum_name,
1154 variant,
1155 args,
1156 } => {
1157 for arg in args {
1158 self.check_node(arg, scope);
1159 }
1160 if let Some(variants) = scope.get_enum(enum_name) {
1161 if !variants.contains(variant) {
1162 self.warning_at(
1163 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1164 span,
1165 );
1166 }
1167 }
1168 }
1169
1170 Node::InterpolatedString(_) => {}
1172
1173 Node::StringLiteral(_)
1175 | Node::RawStringLiteral(_)
1176 | Node::IntLiteral(_)
1177 | Node::FloatLiteral(_)
1178 | Node::BoolLiteral(_)
1179 | Node::NilLiteral
1180 | Node::Identifier(_)
1181 | Node::DurationLiteral(_)
1182 | Node::BreakStmt
1183 | Node::ContinueStmt
1184 | Node::ReturnStmt { value: None }
1185 | Node::ImportDecl { .. }
1186 | Node::SelectiveImport { .. } => {}
1187
1188 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1191 let mut decl_scope = scope.child();
1192 self.check_block(body, &mut decl_scope);
1193 }
1194 }
1195 }
1196
1197 fn check_fn_body(
1198 &mut self,
1199 type_params: &[TypeParam],
1200 params: &[TypedParam],
1201 return_type: &Option<TypeExpr>,
1202 body: &[SNode],
1203 where_clauses: &[WhereClause],
1204 ) {
1205 let mut fn_scope = self.scope.child();
1206 for tp in type_params {
1209 fn_scope.generic_type_params.insert(tp.name.clone());
1210 }
1211 for wc in where_clauses {
1213 fn_scope
1214 .where_constraints
1215 .insert(wc.type_name.clone(), wc.bound.clone());
1216 }
1217 for param in params {
1218 fn_scope.define_var(¶m.name, param.type_expr.clone());
1219 if let Some(default) = ¶m.default_value {
1220 self.check_node(default, &mut fn_scope);
1221 }
1222 }
1223 let ret_scope_base = if return_type.is_some() {
1226 Some(fn_scope.child())
1227 } else {
1228 None
1229 };
1230
1231 self.check_block(body, &mut fn_scope);
1232
1233 if let Some(ret_type) = return_type {
1235 let mut ret_scope = ret_scope_base.unwrap();
1236 for stmt in body {
1237 self.check_return_type(stmt, ret_type, &mut ret_scope);
1238 }
1239 }
1240 }
1241
1242 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1243 let span = snode.span;
1244 match &snode.node {
1245 Node::ReturnStmt { value: Some(val) } => {
1246 let inferred = self.infer_type(val, scope);
1247 if let Some(actual) = &inferred {
1248 if !self.types_compatible(expected, actual, scope) {
1249 self.error_at(
1250 format!(
1251 "Return type mismatch: expected {}, got {}",
1252 format_type(expected),
1253 format_type(actual)
1254 ),
1255 span,
1256 );
1257 }
1258 }
1259 }
1260 Node::IfElse {
1261 condition,
1262 then_body,
1263 else_body,
1264 } => {
1265 let refs = Self::extract_refinements(condition, scope);
1266 let mut then_scope = scope.child();
1267 apply_refinements(&mut then_scope, &refs.truthy);
1268 for stmt in then_body {
1269 self.check_return_type(stmt, expected, &mut then_scope);
1270 }
1271 if let Some(else_body) = else_body {
1272 let mut else_scope = scope.child();
1273 apply_refinements(&mut else_scope, &refs.falsy);
1274 for stmt in else_body {
1275 self.check_return_type(stmt, expected, &mut else_scope);
1276 }
1277 if Self::block_definitely_exits(then_body)
1279 && !Self::block_definitely_exits(else_body)
1280 {
1281 apply_refinements(scope, &refs.falsy);
1282 } else if Self::block_definitely_exits(else_body)
1283 && !Self::block_definitely_exits(then_body)
1284 {
1285 apply_refinements(scope, &refs.truthy);
1286 }
1287 } else {
1288 if Self::block_definitely_exits(then_body) {
1290 apply_refinements(scope, &refs.falsy);
1291 }
1292 }
1293 }
1294 _ => {}
1295 }
1296 }
1297
1298 fn satisfies_interface(
1304 &self,
1305 type_name: &str,
1306 interface_name: &str,
1307 scope: &TypeScope,
1308 ) -> bool {
1309 self.interface_mismatch_reason(type_name, interface_name, scope)
1310 .is_none()
1311 }
1312
1313 fn interface_mismatch_reason(
1316 &self,
1317 type_name: &str,
1318 interface_name: &str,
1319 scope: &TypeScope,
1320 ) -> Option<String> {
1321 let interface_methods = match scope.get_interface(interface_name) {
1322 Some(methods) => methods,
1323 None => return Some(format!("interface '{}' not found", interface_name)),
1324 };
1325 let impl_methods = match scope.get_impl_methods(type_name) {
1326 Some(methods) => methods,
1327 None => {
1328 if interface_methods.is_empty() {
1329 return None;
1330 }
1331 let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1332 return Some(format!("missing method(s): {}", names.join(", ")));
1333 }
1334 };
1335 for iface_method in interface_methods {
1336 let iface_params: Vec<_> = iface_method
1337 .params
1338 .iter()
1339 .filter(|p| p.name != "self")
1340 .collect();
1341 let iface_param_count = iface_params.len();
1342 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1343 let impl_method = match matching_impl {
1344 Some(m) => m,
1345 None => {
1346 return Some(format!("missing method '{}'", iface_method.name));
1347 }
1348 };
1349 if impl_method.param_count != iface_param_count {
1350 return Some(format!(
1351 "method '{}' has {} parameter(s), expected {}",
1352 iface_method.name, impl_method.param_count, iface_param_count
1353 ));
1354 }
1355 for (i, iface_param) in iface_params.iter().enumerate() {
1357 if let (Some(expected), Some(actual)) = (
1358 &iface_param.type_expr,
1359 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1360 ) {
1361 if !self.types_compatible(expected, actual, scope) {
1362 return Some(format!(
1363 "method '{}' parameter {} has type '{}', expected '{}'",
1364 iface_method.name,
1365 i + 1,
1366 format_type(actual),
1367 format_type(expected),
1368 ));
1369 }
1370 }
1371 }
1372 if let (Some(expected_ret), Some(actual_ret)) =
1374 (&iface_method.return_type, &impl_method.return_type)
1375 {
1376 if !self.types_compatible(expected_ret, actual_ret, scope) {
1377 return Some(format!(
1378 "method '{}' returns '{}', expected '{}'",
1379 iface_method.name,
1380 format_type(actual_ret),
1381 format_type(expected_ret),
1382 ));
1383 }
1384 }
1385 }
1386 None
1387 }
1388
1389 fn extract_type_bindings(
1392 param_type: &TypeExpr,
1393 arg_type: &TypeExpr,
1394 type_params: &std::collections::BTreeSet<String>,
1395 bindings: &mut BTreeMap<String, String>,
1396 ) {
1397 match (param_type, arg_type) {
1398 (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1400 if type_params.contains(param_name) =>
1401 {
1402 bindings
1403 .entry(param_name.clone())
1404 .or_insert(concrete.clone());
1405 }
1406 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1408 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1409 }
1410 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1412 Self::extract_type_bindings(pk, ak, type_params, bindings);
1413 Self::extract_type_bindings(pv, av, type_params, bindings);
1414 }
1415 _ => {}
1416 }
1417 }
1418
1419 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1421 match &condition.node {
1422 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1424 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1425 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1426 return nil_ref;
1427 }
1428 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1429 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1430 return typeof_ref;
1431 }
1432 Refinements::empty()
1433 }
1434
1435 Node::BinaryOp { op, left, right } if op == "&&" => {
1437 let left_ref = Self::extract_refinements(left, scope);
1438 let right_ref = Self::extract_refinements(right, scope);
1439 let mut truthy = left_ref.truthy;
1440 truthy.extend(right_ref.truthy);
1441 Refinements {
1442 truthy,
1443 falsy: vec![],
1444 }
1445 }
1446
1447 Node::BinaryOp { op, left, right } if op == "||" => {
1449 let left_ref = Self::extract_refinements(left, scope);
1450 let right_ref = Self::extract_refinements(right, scope);
1451 let mut falsy = left_ref.falsy;
1452 falsy.extend(right_ref.falsy);
1453 Refinements {
1454 truthy: vec![],
1455 falsy,
1456 }
1457 }
1458
1459 Node::UnaryOp { op, operand } if op == "!" => {
1461 Self::extract_refinements(operand, scope).inverted()
1462 }
1463
1464 Node::Identifier(name) => {
1466 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1467 if members
1468 .iter()
1469 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1470 {
1471 if let Some(narrowed) = remove_from_union(members, "nil") {
1472 return Refinements {
1473 truthy: vec![(name.clone(), Some(narrowed))],
1474 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1475 };
1476 }
1477 }
1478 }
1479 Refinements::empty()
1480 }
1481
1482 Node::MethodCall {
1484 object,
1485 method,
1486 args,
1487 } if method == "has" && args.len() == 1 => {
1488 Self::extract_has_refinements(object, args, scope)
1489 }
1490
1491 _ => Refinements::empty(),
1492 }
1493 }
1494
1495 fn extract_nil_refinements(
1497 op: &str,
1498 left: &SNode,
1499 right: &SNode,
1500 scope: &TypeScope,
1501 ) -> Refinements {
1502 let var_node = if matches!(right.node, Node::NilLiteral) {
1503 left
1504 } else if matches!(left.node, Node::NilLiteral) {
1505 right
1506 } else {
1507 return Refinements::empty();
1508 };
1509
1510 if let Node::Identifier(name) = &var_node.node {
1511 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1512 if let Some(narrowed) = remove_from_union(members, "nil") {
1513 let neq_refs = Refinements {
1514 truthy: vec![(name.clone(), Some(narrowed))],
1515 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1516 };
1517 return if op == "!=" {
1518 neq_refs
1519 } else {
1520 neq_refs.inverted()
1521 };
1522 }
1523 }
1524 }
1525 Refinements::empty()
1526 }
1527
1528 fn extract_typeof_refinements(
1530 op: &str,
1531 left: &SNode,
1532 right: &SNode,
1533 scope: &TypeScope,
1534 ) -> Refinements {
1535 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1536 (extract_type_of_var(left), &right.node)
1537 {
1538 (var, tn.clone())
1539 } else if let (Node::StringLiteral(tn), Some(var)) =
1540 (&left.node, extract_type_of_var(right))
1541 {
1542 (var, tn.clone())
1543 } else {
1544 return Refinements::empty();
1545 };
1546
1547 const KNOWN_TYPES: &[&str] = &[
1548 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1549 ];
1550 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1551 return Refinements::empty();
1552 }
1553
1554 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(&var_name) {
1555 let narrowed = narrow_to_single(members, &type_name);
1556 let remaining = remove_from_union(members, &type_name);
1557 if narrowed.is_some() || remaining.is_some() {
1558 let eq_refs = Refinements {
1559 truthy: narrowed
1560 .map(|n| vec![(var_name.clone(), Some(n))])
1561 .unwrap_or_default(),
1562 falsy: remaining
1563 .map(|r| vec![(var_name.clone(), Some(r))])
1564 .unwrap_or_default(),
1565 };
1566 return if op == "==" {
1567 eq_refs
1568 } else {
1569 eq_refs.inverted()
1570 };
1571 }
1572 }
1573 Refinements::empty()
1574 }
1575
1576 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1578 if let Node::Identifier(var_name) = &object.node {
1579 if let Node::StringLiteral(key) = &args[0].node {
1580 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1581 if fields.iter().any(|f| f.name == *key && f.optional) {
1582 let narrowed_fields: Vec<ShapeField> = fields
1583 .iter()
1584 .map(|f| {
1585 if f.name == *key {
1586 ShapeField {
1587 name: f.name.clone(),
1588 type_expr: f.type_expr.clone(),
1589 optional: false,
1590 }
1591 } else {
1592 f.clone()
1593 }
1594 })
1595 .collect();
1596 return Refinements {
1597 truthy: vec![(
1598 var_name.clone(),
1599 Some(TypeExpr::Shape(narrowed_fields)),
1600 )],
1601 falsy: vec![],
1602 };
1603 }
1604 }
1605 }
1606 }
1607 Refinements::empty()
1608 }
1609
1610 fn block_definitely_exits(stmts: &[SNode]) -> bool {
1612 stmts.iter().any(|s| match &s.node {
1613 Node::ReturnStmt { .. }
1614 | Node::ThrowStmt { .. }
1615 | Node::BreakStmt
1616 | Node::ContinueStmt => true,
1617 Node::IfElse {
1618 then_body,
1619 else_body: Some(else_body),
1620 ..
1621 } => Self::block_definitely_exits(then_body) && Self::block_definitely_exits(else_body),
1622 _ => false,
1623 })
1624 }
1625
1626 fn check_match_exhaustiveness(
1627 &mut self,
1628 value: &SNode,
1629 arms: &[MatchArm],
1630 scope: &TypeScope,
1631 span: Span,
1632 ) {
1633 let enum_name = match &value.node {
1635 Node::PropertyAccess { object, property } if property == "variant" => {
1636 match self.infer_type(object, scope) {
1638 Some(TypeExpr::Named(name)) => {
1639 if scope.get_enum(&name).is_some() {
1640 Some(name)
1641 } else {
1642 None
1643 }
1644 }
1645 _ => None,
1646 }
1647 }
1648 _ => {
1649 match self.infer_type(value, scope) {
1651 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1652 _ => None,
1653 }
1654 }
1655 };
1656
1657 let Some(enum_name) = enum_name else {
1658 return;
1659 };
1660 let Some(variants) = scope.get_enum(&enum_name) else {
1661 return;
1662 };
1663
1664 let mut covered: Vec<String> = Vec::new();
1666 let mut has_wildcard = false;
1667
1668 for arm in arms {
1669 match &arm.pattern.node {
1670 Node::StringLiteral(s) => covered.push(s.clone()),
1672 Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1674 has_wildcard = true;
1675 }
1676 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1678 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1680 _ => {
1681 has_wildcard = true;
1683 }
1684 }
1685 }
1686
1687 if has_wildcard {
1688 return;
1689 }
1690
1691 let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1692 if !missing.is_empty() {
1693 let missing_str = missing
1694 .iter()
1695 .map(|s| format!("\"{}\"", s))
1696 .collect::<Vec<_>>()
1697 .join(", ");
1698 self.warning_at(
1699 format!(
1700 "Non-exhaustive match on enum {}: missing variants {}",
1701 enum_name, missing_str
1702 ),
1703 span,
1704 );
1705 }
1706 }
1707
1708 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1709 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1711 if let Some(sig) = scope.get_fn(name).cloned() {
1712 if !has_spread
1713 && !is_builtin(name)
1714 && !sig.has_rest
1715 && (args.len() < sig.required_params || args.len() > sig.params.len())
1716 {
1717 let expected = if sig.required_params == sig.params.len() {
1718 format!("{}", sig.params.len())
1719 } else {
1720 format!("{}-{}", sig.required_params, sig.params.len())
1721 };
1722 self.warning_at(
1723 format!(
1724 "Function '{}' expects {} arguments, got {}",
1725 name,
1726 expected,
1727 args.len()
1728 ),
1729 span,
1730 );
1731 }
1732 let call_scope = if sig.type_param_names.is_empty() {
1735 scope.clone()
1736 } else {
1737 let mut s = scope.child();
1738 for tp_name in &sig.type_param_names {
1739 s.generic_type_params.insert(tp_name.clone());
1740 }
1741 s
1742 };
1743 for (i, (arg, (param_name, param_type))) in
1744 args.iter().zip(sig.params.iter()).enumerate()
1745 {
1746 if let Some(expected) = param_type {
1747 let actual = self.infer_type(arg, scope);
1748 if let Some(actual) = &actual {
1749 if !self.types_compatible(expected, actual, &call_scope) {
1750 self.error_at(
1751 format!(
1752 "Argument {} ('{}'): expected {}, got {}",
1753 i + 1,
1754 param_name,
1755 format_type(expected),
1756 format_type(actual)
1757 ),
1758 arg.span,
1759 );
1760 }
1761 }
1762 }
1763 }
1764 if !sig.where_clauses.is_empty() {
1766 let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1769 let type_param_set: std::collections::BTreeSet<String> =
1770 sig.type_param_names.iter().cloned().collect();
1771 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1772 if let Some(param_ty) = param_type {
1773 if let Some(arg_ty) = self.infer_type(arg, scope) {
1774 Self::extract_type_bindings(
1775 param_ty,
1776 &arg_ty,
1777 &type_param_set,
1778 &mut type_bindings,
1779 );
1780 }
1781 }
1782 }
1783 for (type_param, bound) in &sig.where_clauses {
1784 if let Some(concrete_type) = type_bindings.get(type_param) {
1785 if let Some(reason) =
1786 self.interface_mismatch_reason(concrete_type, bound, scope)
1787 {
1788 self.warning_at(
1789 format!(
1790 "Type '{}' does not satisfy interface '{}': {} \
1791 (required by constraint `where {}: {}`)",
1792 concrete_type, bound, reason, type_param, bound
1793 ),
1794 span,
1795 );
1796 }
1797 }
1798 }
1799 }
1800 }
1801 for arg in args {
1803 self.check_node(arg, scope);
1804 }
1805 }
1806
1807 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1809 match &snode.node {
1810 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1811 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1812 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1813 Some(TypeExpr::Named("string".into()))
1814 }
1815 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1816 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1817 Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1818 Node::DictLiteral(entries) => {
1819 let mut fields = Vec::new();
1821 let mut all_string_keys = true;
1822 for entry in entries {
1823 if let Node::StringLiteral(key) = &entry.key.node {
1824 let val_type = self
1825 .infer_type(&entry.value, scope)
1826 .unwrap_or(TypeExpr::Named("nil".into()));
1827 fields.push(ShapeField {
1828 name: key.clone(),
1829 type_expr: val_type,
1830 optional: false,
1831 });
1832 } else {
1833 all_string_keys = false;
1834 break;
1835 }
1836 }
1837 if all_string_keys && !fields.is_empty() {
1838 Some(TypeExpr::Shape(fields))
1839 } else {
1840 Some(TypeExpr::Named("dict".into()))
1841 }
1842 }
1843 Node::Closure { params, body, .. } => {
1844 let all_typed = params.iter().all(|p| p.type_expr.is_some());
1846 if all_typed && !params.is_empty() {
1847 let param_types: Vec<TypeExpr> =
1848 params.iter().filter_map(|p| p.type_expr.clone()).collect();
1849 let ret = body.last().and_then(|last| self.infer_type(last, scope));
1851 if let Some(ret_type) = ret {
1852 return Some(TypeExpr::FnType {
1853 params: param_types,
1854 return_type: Box::new(ret_type),
1855 });
1856 }
1857 }
1858 Some(TypeExpr::Named("closure".into()))
1859 }
1860
1861 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1862
1863 Node::FunctionCall { name, .. } => {
1864 if scope.get_struct(name).is_some() {
1866 return Some(TypeExpr::Named(name.clone()));
1867 }
1868 if let Some(sig) = scope.get_fn(name) {
1870 return sig.return_type.clone();
1871 }
1872 builtin_return_type(name)
1874 }
1875
1876 Node::BinaryOp { op, left, right } => {
1877 let lt = self.infer_type(left, scope);
1878 let rt = self.infer_type(right, scope);
1879 infer_binary_op_type(op, <, &rt)
1880 }
1881
1882 Node::UnaryOp { op, operand } => {
1883 let t = self.infer_type(operand, scope);
1884 match op.as_str() {
1885 "!" => Some(TypeExpr::Named("bool".into())),
1886 "-" => t, _ => None,
1888 }
1889 }
1890
1891 Node::Ternary {
1892 condition,
1893 true_expr,
1894 false_expr,
1895 } => {
1896 let refs = Self::extract_refinements(condition, scope);
1897
1898 let mut true_scope = scope.child();
1899 apply_refinements(&mut true_scope, &refs.truthy);
1900 let tt = self.infer_type(true_expr, &true_scope);
1901
1902 let mut false_scope = scope.child();
1903 apply_refinements(&mut false_scope, &refs.falsy);
1904 let ft = self.infer_type(false_expr, &false_scope);
1905
1906 match (&tt, &ft) {
1907 (Some(a), Some(b)) if a == b => tt,
1908 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1909 (Some(_), None) => tt,
1910 (None, Some(_)) => ft,
1911 (None, None) => None,
1912 }
1913 }
1914
1915 Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1916
1917 Node::PropertyAccess { object, property } => {
1918 if let Node::Identifier(name) = &object.node {
1920 if scope.get_enum(name).is_some() {
1921 return Some(TypeExpr::Named(name.clone()));
1922 }
1923 }
1924 if property == "variant" {
1926 let obj_type = self.infer_type(object, scope);
1927 if let Some(TypeExpr::Named(name)) = &obj_type {
1928 if scope.get_enum(name).is_some() {
1929 return Some(TypeExpr::Named("string".into()));
1930 }
1931 }
1932 }
1933 let obj_type = self.infer_type(object, scope);
1935 if let Some(TypeExpr::Shape(fields)) = &obj_type {
1936 if let Some(field) = fields.iter().find(|f| f.name == *property) {
1937 return Some(field.type_expr.clone());
1938 }
1939 }
1940 None
1941 }
1942
1943 Node::SubscriptAccess { object, index } => {
1944 let obj_type = self.infer_type(object, scope);
1945 match &obj_type {
1946 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1947 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1948 Some(TypeExpr::Shape(fields)) => {
1949 if let Node::StringLiteral(key) = &index.node {
1951 fields
1952 .iter()
1953 .find(|f| &f.name == key)
1954 .map(|f| f.type_expr.clone())
1955 } else {
1956 None
1957 }
1958 }
1959 Some(TypeExpr::Named(n)) if n == "list" => None,
1960 Some(TypeExpr::Named(n)) if n == "dict" => None,
1961 Some(TypeExpr::Named(n)) if n == "string" => {
1962 Some(TypeExpr::Named("string".into()))
1963 }
1964 _ => None,
1965 }
1966 }
1967 Node::SliceAccess { object, .. } => {
1968 let obj_type = self.infer_type(object, scope);
1970 match &obj_type {
1971 Some(TypeExpr::List(_)) => obj_type,
1972 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1973 Some(TypeExpr::Named(n)) if n == "string" => {
1974 Some(TypeExpr::Named("string".into()))
1975 }
1976 _ => None,
1977 }
1978 }
1979 Node::MethodCall { object, method, .. }
1980 | Node::OptionalMethodCall { object, method, .. } => {
1981 let obj_type = self.infer_type(object, scope);
1982 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
1983 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
1984 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
1985 match method.as_str() {
1986 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
1988 Some(TypeExpr::Named("bool".into()))
1989 }
1990 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
1992 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
1994 | "pad_left" | "pad_right" | "repeat" | "join" => {
1995 Some(TypeExpr::Named("string".into()))
1996 }
1997 "split" | "chars" => Some(TypeExpr::Named("list".into())),
1998 "filter" => {
2000 if is_dict {
2001 Some(TypeExpr::Named("dict".into()))
2002 } else {
2003 Some(TypeExpr::Named("list".into()))
2004 }
2005 }
2006 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2008 "reduce" | "find" | "first" | "last" => None,
2009 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2011 "merge" | "map_values" | "rekey" | "map_keys" => {
2012 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2016 Some(TypeExpr::DictType(
2017 Box::new(TypeExpr::Named("string".into())),
2018 v.clone(),
2019 ))
2020 } else {
2021 Some(TypeExpr::Named("dict".into()))
2022 }
2023 }
2024 "to_string" => Some(TypeExpr::Named("string".into())),
2026 "to_int" => Some(TypeExpr::Named("int".into())),
2027 "to_float" => Some(TypeExpr::Named("float".into())),
2028 _ => None,
2029 }
2030 }
2031
2032 Node::TryOperator { operand } => {
2034 match self.infer_type(operand, scope) {
2035 Some(TypeExpr::Named(name)) if name == "Result" => None, _ => None,
2037 }
2038 }
2039
2040 _ => None,
2041 }
2042 }
2043
2044 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2046 if let TypeExpr::Named(name) = expected {
2048 if scope.is_generic_type_param(name) {
2049 return true;
2050 }
2051 }
2052 if let TypeExpr::Named(name) = actual {
2053 if scope.is_generic_type_param(name) {
2054 return true;
2055 }
2056 }
2057 let expected = self.resolve_alias(expected, scope);
2058 let actual = self.resolve_alias(actual, scope);
2059
2060 if let TypeExpr::Named(iface_name) = &expected {
2063 if scope.get_interface(iface_name).is_some() {
2064 if let TypeExpr::Named(type_name) = &actual {
2065 return self.satisfies_interface(type_name, iface_name, scope);
2066 }
2067 return false;
2068 }
2069 }
2070
2071 match (&expected, &actual) {
2072 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2073 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2076 act_members.iter().all(|am| {
2077 exp_members
2078 .iter()
2079 .any(|em| self.types_compatible(em, am, scope))
2080 })
2081 }
2082 (TypeExpr::Union(members), actual_type) => members
2083 .iter()
2084 .any(|m| self.types_compatible(m, actual_type, scope)),
2085 (expected_type, TypeExpr::Union(members)) => members
2086 .iter()
2087 .all(|m| self.types_compatible(expected_type, m, scope)),
2088 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2089 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2090 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2091 if expected_field.optional {
2092 return true;
2093 }
2094 af.iter().any(|actual_field| {
2095 actual_field.name == expected_field.name
2096 && self.types_compatible(
2097 &expected_field.type_expr,
2098 &actual_field.type_expr,
2099 scope,
2100 )
2101 })
2102 }),
2103 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2105 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2106 keys_ok
2107 && af
2108 .iter()
2109 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2110 }
2111 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2113 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2114 self.types_compatible(expected_inner, actual_inner, scope)
2115 }
2116 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2117 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2118 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2119 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2120 }
2121 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2122 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2123 (
2125 TypeExpr::FnType {
2126 params: ep,
2127 return_type: er,
2128 },
2129 TypeExpr::FnType {
2130 params: ap,
2131 return_type: ar,
2132 },
2133 ) => {
2134 ep.len() == ap.len()
2135 && ep
2136 .iter()
2137 .zip(ap.iter())
2138 .all(|(e, a)| self.types_compatible(e, a, scope))
2139 && self.types_compatible(er, ar, scope)
2140 }
2141 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2143 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2144 _ => false,
2145 }
2146 }
2147
2148 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2149 if let TypeExpr::Named(name) = ty {
2150 if let Some(resolved) = scope.resolve_type(name) {
2151 return resolved.clone();
2152 }
2153 }
2154 ty.clone()
2155 }
2156
2157 fn error_at(&mut self, message: String, span: Span) {
2158 self.diagnostics.push(TypeDiagnostic {
2159 message,
2160 severity: DiagnosticSeverity::Error,
2161 span: Some(span),
2162 help: None,
2163 });
2164 }
2165
2166 #[allow(dead_code)]
2167 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2168 self.diagnostics.push(TypeDiagnostic {
2169 message,
2170 severity: DiagnosticSeverity::Error,
2171 span: Some(span),
2172 help: Some(help),
2173 });
2174 }
2175
2176 fn warning_at(&mut self, message: String, span: Span) {
2177 self.diagnostics.push(TypeDiagnostic {
2178 message,
2179 severity: DiagnosticSeverity::Warning,
2180 span: Some(span),
2181 help: None,
2182 });
2183 }
2184
2185 #[allow(dead_code)]
2186 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2187 self.diagnostics.push(TypeDiagnostic {
2188 message,
2189 severity: DiagnosticSeverity::Warning,
2190 span: Some(span),
2191 help: Some(help),
2192 });
2193 }
2194}
2195
2196impl Default for TypeChecker {
2197 fn default() -> Self {
2198 Self::new()
2199 }
2200}
2201
2202fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2204 match op {
2205 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2206 Some(TypeExpr::Named("bool".into()))
2207 }
2208 "+" => match (left, right) {
2209 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2210 match (l.as_str(), r.as_str()) {
2211 ("int", "int") => Some(TypeExpr::Named("int".into())),
2212 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2213 ("string", "string") => Some(TypeExpr::Named("string".into())),
2214 ("list", "list") => Some(TypeExpr::Named("list".into())),
2215 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2216 _ => None,
2217 }
2218 }
2219 _ => None,
2220 },
2221 "-" | "/" | "%" => match (left, right) {
2222 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2223 match (l.as_str(), r.as_str()) {
2224 ("int", "int") => Some(TypeExpr::Named("int".into())),
2225 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2226 _ => None,
2227 }
2228 }
2229 _ => None,
2230 },
2231 "*" => match (left, right) {
2232 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2233 match (l.as_str(), r.as_str()) {
2234 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2235 ("int", "int") => Some(TypeExpr::Named("int".into())),
2236 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2237 _ => None,
2238 }
2239 }
2240 _ => None,
2241 },
2242 "??" => match (left, right) {
2243 (Some(TypeExpr::Union(members)), _) => {
2244 let non_nil: Vec<_> = members
2245 .iter()
2246 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2247 .cloned()
2248 .collect();
2249 if non_nil.len() == 1 {
2250 Some(non_nil[0].clone())
2251 } else if non_nil.is_empty() {
2252 right.clone()
2253 } else {
2254 Some(TypeExpr::Union(non_nil))
2255 }
2256 }
2257 _ => right.clone(),
2258 },
2259 "|>" => None,
2260 _ => None,
2261 }
2262}
2263
2264pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2269 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2270 let mut details = Vec::new();
2271 for field in ef {
2272 if field.optional {
2273 continue;
2274 }
2275 match af.iter().find(|f| f.name == field.name) {
2276 None => details.push(format!(
2277 "missing field '{}' ({})",
2278 field.name,
2279 format_type(&field.type_expr)
2280 )),
2281 Some(actual_field) => {
2282 let e_str = format_type(&field.type_expr);
2283 let a_str = format_type(&actual_field.type_expr);
2284 if e_str != a_str {
2285 details.push(format!(
2286 "field '{}' has type {}, expected {}",
2287 field.name, a_str, e_str
2288 ));
2289 }
2290 }
2291 }
2292 }
2293 if details.is_empty() {
2294 None
2295 } else {
2296 Some(details.join("; "))
2297 }
2298 } else {
2299 None
2300 }
2301}
2302
2303pub fn format_type(ty: &TypeExpr) -> String {
2304 match ty {
2305 TypeExpr::Named(n) => n.clone(),
2306 TypeExpr::Union(types) => types
2307 .iter()
2308 .map(format_type)
2309 .collect::<Vec<_>>()
2310 .join(" | "),
2311 TypeExpr::Shape(fields) => {
2312 let inner: Vec<String> = fields
2313 .iter()
2314 .map(|f| {
2315 let opt = if f.optional { "?" } else { "" };
2316 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2317 })
2318 .collect();
2319 format!("{{{}}}", inner.join(", "))
2320 }
2321 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2322 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2323 TypeExpr::FnType {
2324 params,
2325 return_type,
2326 } => {
2327 let params_str = params
2328 .iter()
2329 .map(format_type)
2330 .collect::<Vec<_>>()
2331 .join(", ");
2332 format!("fn({}) -> {}", params_str, format_type(return_type))
2333 }
2334 }
2335}
2336
2337fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2339 let remaining: Vec<TypeExpr> = members
2340 .iter()
2341 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2342 .cloned()
2343 .collect();
2344 match remaining.len() {
2345 0 => None,
2346 1 => Some(remaining.into_iter().next().unwrap()),
2347 _ => Some(TypeExpr::Union(remaining)),
2348 }
2349}
2350
2351fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2353 if members
2354 .iter()
2355 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2356 {
2357 Some(TypeExpr::Named(target.to_string()))
2358 } else {
2359 None
2360 }
2361}
2362
2363fn extract_type_of_var(node: &SNode) -> Option<String> {
2365 if let Node::FunctionCall { name, args } = &node.node {
2366 if name == "type_of" && args.len() == 1 {
2367 if let Node::Identifier(var) = &args[0].node {
2368 return Some(var.clone());
2369 }
2370 }
2371 }
2372 None
2373}
2374
2375fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
2377 for (var_name, narrowed_type) in refinements {
2378 if !scope.narrowed_vars.contains_key(var_name) {
2380 if let Some(original) = scope.get_var(var_name).cloned() {
2381 scope.narrowed_vars.insert(var_name.clone(), original);
2382 }
2383 }
2384 scope.define_var(var_name, narrowed_type.clone());
2385 }
2386}
2387
2388#[cfg(test)]
2389mod tests {
2390 use super::*;
2391 use crate::Parser;
2392 use harn_lexer::Lexer;
2393
2394 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
2395 let mut lexer = Lexer::new(source);
2396 let tokens = lexer.tokenize().unwrap();
2397 let mut parser = Parser::new(tokens);
2398 let program = parser.parse().unwrap();
2399 TypeChecker::new().check(&program)
2400 }
2401
2402 fn errors(source: &str) -> Vec<String> {
2403 check_source(source)
2404 .into_iter()
2405 .filter(|d| d.severity == DiagnosticSeverity::Error)
2406 .map(|d| d.message)
2407 .collect()
2408 }
2409
2410 #[test]
2411 fn test_no_errors_for_untyped_code() {
2412 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
2413 assert!(errs.is_empty());
2414 }
2415
2416 #[test]
2417 fn test_correct_typed_let() {
2418 let errs = errors("pipeline t(task) { let x: int = 42 }");
2419 assert!(errs.is_empty());
2420 }
2421
2422 #[test]
2423 fn test_type_mismatch_let() {
2424 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
2425 assert_eq!(errs.len(), 1);
2426 assert!(errs[0].contains("Type mismatch"));
2427 assert!(errs[0].contains("int"));
2428 assert!(errs[0].contains("string"));
2429 }
2430
2431 #[test]
2432 fn test_correct_typed_fn() {
2433 let errs = errors(
2434 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
2435 );
2436 assert!(errs.is_empty());
2437 }
2438
2439 #[test]
2440 fn test_fn_arg_type_mismatch() {
2441 let errs = errors(
2442 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
2443add("hello", 2) }"#,
2444 );
2445 assert_eq!(errs.len(), 1);
2446 assert!(errs[0].contains("Argument 1"));
2447 assert!(errs[0].contains("expected int"));
2448 }
2449
2450 #[test]
2451 fn test_return_type_mismatch() {
2452 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2453 assert_eq!(errs.len(), 1);
2454 assert!(errs[0].contains("Return type mismatch"));
2455 }
2456
2457 #[test]
2458 fn test_union_type_compatible() {
2459 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2460 assert!(errs.is_empty());
2461 }
2462
2463 #[test]
2464 fn test_union_type_mismatch() {
2465 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2466 assert_eq!(errs.len(), 1);
2467 assert!(errs[0].contains("Type mismatch"));
2468 }
2469
2470 #[test]
2471 fn test_type_inference_propagation() {
2472 let errs = errors(
2473 r#"pipeline t(task) {
2474 fn add(a: int, b: int) -> int { return a + b }
2475 let result: string = add(1, 2)
2476}"#,
2477 );
2478 assert_eq!(errs.len(), 1);
2479 assert!(errs[0].contains("Type mismatch"));
2480 assert!(errs[0].contains("string"));
2481 assert!(errs[0].contains("int"));
2482 }
2483
2484 #[test]
2485 fn test_builtin_return_type_inference() {
2486 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2487 assert_eq!(errs.len(), 1);
2488 assert!(errs[0].contains("string"));
2489 assert!(errs[0].contains("int"));
2490 }
2491
2492 #[test]
2493 fn test_workflow_and_transcript_builtins_are_known() {
2494 let errs = errors(
2495 r#"pipeline t(task) {
2496 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2497 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2498 let run: dict = workflow_execute("task", flow, [], {})
2499 let tree: dict = load_run_tree("run.json")
2500 let fixture: dict = run_record_fixture(run?.run)
2501 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
2502 let diff: dict = run_record_diff(run?.run, run?.run)
2503 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
2504 let suite_report: dict = eval_suite_run(manifest)
2505 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
2506 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
2507 let selection: dict = artifact_editor_selection("src/main.rs", "main")
2508 let verify: dict = artifact_verification_result("verify", "ok")
2509 let test_result: dict = artifact_test_result("tests", "pass")
2510 let cmd: dict = artifact_command_result("cargo test", {status: 0})
2511 let patch: dict = artifact_diff("src/main.rs", "old", "new")
2512 let git: dict = artifact_git_diff("diff --git a b")
2513 let review: dict = artifact_diff_review(patch, "review me")
2514 let decision: dict = artifact_review_decision(review, "accepted")
2515 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
2516 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
2517 let apply: dict = artifact_apply_intent(review, "apply")
2518 let transcript = transcript_reset({metadata: {source: "test"}})
2519 let visible: string = transcript_render_visible(transcript_archive(transcript))
2520 let events: list = transcript_events(transcript)
2521 let context: string = artifact_context([], {max_artifacts: 1})
2522 println(report)
2523 println(run)
2524 println(tree)
2525 println(fixture)
2526 println(suite)
2527 println(diff)
2528 println(manifest)
2529 println(suite_report)
2530 println(wf)
2531 println(snap)
2532 println(selection)
2533 println(verify)
2534 println(test_result)
2535 println(cmd)
2536 println(patch)
2537 println(git)
2538 println(review)
2539 println(decision)
2540 println(proposal)
2541 println(bundle)
2542 println(apply)
2543 println(visible)
2544 println(events)
2545 println(context)
2546}"#,
2547 );
2548 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2549 }
2550
2551 #[test]
2552 fn test_binary_op_type_inference() {
2553 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
2554 assert_eq!(errs.len(), 1);
2555 }
2556
2557 #[test]
2558 fn test_comparison_returns_bool() {
2559 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
2560 assert!(errs.is_empty());
2561 }
2562
2563 #[test]
2564 fn test_int_float_promotion() {
2565 let errs = errors("pipeline t(task) { let x: float = 42 }");
2566 assert!(errs.is_empty());
2567 }
2568
2569 #[test]
2570 fn test_untyped_code_no_errors() {
2571 let errs = errors(
2572 r#"pipeline t(task) {
2573 fn process(data) {
2574 let result = data + " processed"
2575 return result
2576 }
2577 log(process("hello"))
2578}"#,
2579 );
2580 assert!(errs.is_empty());
2581 }
2582
2583 #[test]
2584 fn test_type_alias() {
2585 let errs = errors(
2586 r#"pipeline t(task) {
2587 type Name = string
2588 let x: Name = "hello"
2589}"#,
2590 );
2591 assert!(errs.is_empty());
2592 }
2593
2594 #[test]
2595 fn test_type_alias_mismatch() {
2596 let errs = errors(
2597 r#"pipeline t(task) {
2598 type Name = string
2599 let x: Name = 42
2600}"#,
2601 );
2602 assert_eq!(errs.len(), 1);
2603 }
2604
2605 #[test]
2606 fn test_assignment_type_check() {
2607 let errs = errors(
2608 r#"pipeline t(task) {
2609 var x: int = 0
2610 x = "hello"
2611}"#,
2612 );
2613 assert_eq!(errs.len(), 1);
2614 assert!(errs[0].contains("cannot assign string"));
2615 }
2616
2617 #[test]
2618 fn test_covariance_int_to_float_in_fn() {
2619 let errs = errors(
2620 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
2621 );
2622 assert!(errs.is_empty());
2623 }
2624
2625 #[test]
2626 fn test_covariance_return_type() {
2627 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
2628 assert!(errs.is_empty());
2629 }
2630
2631 #[test]
2632 fn test_no_contravariance_float_to_int() {
2633 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
2634 assert_eq!(errs.len(), 1);
2635 }
2636
2637 fn warnings(source: &str) -> Vec<String> {
2640 check_source(source)
2641 .into_iter()
2642 .filter(|d| d.severity == DiagnosticSeverity::Warning)
2643 .map(|d| d.message)
2644 .collect()
2645 }
2646
2647 #[test]
2648 fn test_exhaustive_match_no_warning() {
2649 let warns = warnings(
2650 r#"pipeline t(task) {
2651 enum Color { Red, Green, Blue }
2652 let c = Color.Red
2653 match c.variant {
2654 "Red" -> { log("r") }
2655 "Green" -> { log("g") }
2656 "Blue" -> { log("b") }
2657 }
2658}"#,
2659 );
2660 let exhaustive_warns: Vec<_> = warns
2661 .iter()
2662 .filter(|w| w.contains("Non-exhaustive"))
2663 .collect();
2664 assert!(exhaustive_warns.is_empty());
2665 }
2666
2667 #[test]
2668 fn test_non_exhaustive_match_warning() {
2669 let warns = warnings(
2670 r#"pipeline t(task) {
2671 enum Color { Red, Green, Blue }
2672 let c = Color.Red
2673 match c.variant {
2674 "Red" -> { log("r") }
2675 "Green" -> { log("g") }
2676 }
2677}"#,
2678 );
2679 let exhaustive_warns: Vec<_> = warns
2680 .iter()
2681 .filter(|w| w.contains("Non-exhaustive"))
2682 .collect();
2683 assert_eq!(exhaustive_warns.len(), 1);
2684 assert!(exhaustive_warns[0].contains("Blue"));
2685 }
2686
2687 #[test]
2688 fn test_non_exhaustive_multiple_missing() {
2689 let warns = warnings(
2690 r#"pipeline t(task) {
2691 enum Status { Active, Inactive, Pending }
2692 let s = Status.Active
2693 match s.variant {
2694 "Active" -> { log("a") }
2695 }
2696}"#,
2697 );
2698 let exhaustive_warns: Vec<_> = warns
2699 .iter()
2700 .filter(|w| w.contains("Non-exhaustive"))
2701 .collect();
2702 assert_eq!(exhaustive_warns.len(), 1);
2703 assert!(exhaustive_warns[0].contains("Inactive"));
2704 assert!(exhaustive_warns[0].contains("Pending"));
2705 }
2706
2707 #[test]
2708 fn test_enum_construct_type_inference() {
2709 let errs = errors(
2710 r#"pipeline t(task) {
2711 enum Color { Red, Green, Blue }
2712 let c: Color = Color.Red
2713}"#,
2714 );
2715 assert!(errs.is_empty());
2716 }
2717
2718 #[test]
2721 fn test_nil_coalescing_strips_nil() {
2722 let errs = errors(
2724 r#"pipeline t(task) {
2725 let x: string | nil = nil
2726 let y: string = x ?? "default"
2727}"#,
2728 );
2729 assert!(errs.is_empty());
2730 }
2731
2732 #[test]
2733 fn test_shape_mismatch_detail_missing_field() {
2734 let errs = errors(
2735 r#"pipeline t(task) {
2736 let x: {name: string, age: int} = {name: "hello"}
2737}"#,
2738 );
2739 assert_eq!(errs.len(), 1);
2740 assert!(
2741 errs[0].contains("missing field 'age'"),
2742 "expected detail about missing field, got: {}",
2743 errs[0]
2744 );
2745 }
2746
2747 #[test]
2748 fn test_shape_mismatch_detail_wrong_type() {
2749 let errs = errors(
2750 r#"pipeline t(task) {
2751 let x: {name: string, age: int} = {name: 42, age: 10}
2752}"#,
2753 );
2754 assert_eq!(errs.len(), 1);
2755 assert!(
2756 errs[0].contains("field 'name' has type int, expected string"),
2757 "expected detail about wrong type, got: {}",
2758 errs[0]
2759 );
2760 }
2761
2762 #[test]
2765 fn test_match_pattern_string_against_int() {
2766 let warns = warnings(
2767 r#"pipeline t(task) {
2768 let x: int = 42
2769 match x {
2770 "hello" -> { log("bad") }
2771 42 -> { log("ok") }
2772 }
2773}"#,
2774 );
2775 let pattern_warns: Vec<_> = warns
2776 .iter()
2777 .filter(|w| w.contains("Match pattern type mismatch"))
2778 .collect();
2779 assert_eq!(pattern_warns.len(), 1);
2780 assert!(pattern_warns[0].contains("matching int against string literal"));
2781 }
2782
2783 #[test]
2784 fn test_match_pattern_int_against_string() {
2785 let warns = warnings(
2786 r#"pipeline t(task) {
2787 let x: string = "hello"
2788 match x {
2789 42 -> { log("bad") }
2790 "hello" -> { log("ok") }
2791 }
2792}"#,
2793 );
2794 let pattern_warns: Vec<_> = warns
2795 .iter()
2796 .filter(|w| w.contains("Match pattern type mismatch"))
2797 .collect();
2798 assert_eq!(pattern_warns.len(), 1);
2799 assert!(pattern_warns[0].contains("matching string against int literal"));
2800 }
2801
2802 #[test]
2803 fn test_match_pattern_bool_against_int() {
2804 let warns = warnings(
2805 r#"pipeline t(task) {
2806 let x: int = 42
2807 match x {
2808 true -> { log("bad") }
2809 42 -> { log("ok") }
2810 }
2811}"#,
2812 );
2813 let pattern_warns: Vec<_> = warns
2814 .iter()
2815 .filter(|w| w.contains("Match pattern type mismatch"))
2816 .collect();
2817 assert_eq!(pattern_warns.len(), 1);
2818 assert!(pattern_warns[0].contains("matching int against bool literal"));
2819 }
2820
2821 #[test]
2822 fn test_match_pattern_float_against_string() {
2823 let warns = warnings(
2824 r#"pipeline t(task) {
2825 let x: string = "hello"
2826 match x {
2827 3.14 -> { log("bad") }
2828 "hello" -> { log("ok") }
2829 }
2830}"#,
2831 );
2832 let pattern_warns: Vec<_> = warns
2833 .iter()
2834 .filter(|w| w.contains("Match pattern type mismatch"))
2835 .collect();
2836 assert_eq!(pattern_warns.len(), 1);
2837 assert!(pattern_warns[0].contains("matching string against float literal"));
2838 }
2839
2840 #[test]
2841 fn test_match_pattern_int_against_float_ok() {
2842 let warns = warnings(
2844 r#"pipeline t(task) {
2845 let x: float = 3.14
2846 match x {
2847 42 -> { log("ok") }
2848 _ -> { log("default") }
2849 }
2850}"#,
2851 );
2852 let pattern_warns: Vec<_> = warns
2853 .iter()
2854 .filter(|w| w.contains("Match pattern type mismatch"))
2855 .collect();
2856 assert!(pattern_warns.is_empty());
2857 }
2858
2859 #[test]
2860 fn test_match_pattern_float_against_int_ok() {
2861 let warns = warnings(
2863 r#"pipeline t(task) {
2864 let x: int = 42
2865 match x {
2866 3.14 -> { log("close") }
2867 _ -> { log("default") }
2868 }
2869}"#,
2870 );
2871 let pattern_warns: Vec<_> = warns
2872 .iter()
2873 .filter(|w| w.contains("Match pattern type mismatch"))
2874 .collect();
2875 assert!(pattern_warns.is_empty());
2876 }
2877
2878 #[test]
2879 fn test_match_pattern_correct_types_no_warning() {
2880 let warns = warnings(
2881 r#"pipeline t(task) {
2882 let x: int = 42
2883 match x {
2884 1 -> { log("one") }
2885 2 -> { log("two") }
2886 _ -> { log("other") }
2887 }
2888}"#,
2889 );
2890 let pattern_warns: Vec<_> = warns
2891 .iter()
2892 .filter(|w| w.contains("Match pattern type mismatch"))
2893 .collect();
2894 assert!(pattern_warns.is_empty());
2895 }
2896
2897 #[test]
2898 fn test_match_pattern_wildcard_no_warning() {
2899 let warns = warnings(
2900 r#"pipeline t(task) {
2901 let x: int = 42
2902 match x {
2903 _ -> { log("catch all") }
2904 }
2905}"#,
2906 );
2907 let pattern_warns: Vec<_> = warns
2908 .iter()
2909 .filter(|w| w.contains("Match pattern type mismatch"))
2910 .collect();
2911 assert!(pattern_warns.is_empty());
2912 }
2913
2914 #[test]
2915 fn test_match_pattern_untyped_no_warning() {
2916 let warns = warnings(
2918 r#"pipeline t(task) {
2919 let x = some_unknown_fn()
2920 match x {
2921 "hello" -> { log("string") }
2922 42 -> { log("int") }
2923 }
2924}"#,
2925 );
2926 let pattern_warns: Vec<_> = warns
2927 .iter()
2928 .filter(|w| w.contains("Match pattern type mismatch"))
2929 .collect();
2930 assert!(pattern_warns.is_empty());
2931 }
2932
2933 fn iface_warns(source: &str) -> Vec<String> {
2936 warnings(source)
2937 .into_iter()
2938 .filter(|w| w.contains("does not satisfy interface"))
2939 .collect()
2940 }
2941
2942 #[test]
2943 fn test_interface_constraint_return_type_mismatch() {
2944 let warns = iface_warns(
2945 r#"pipeline t(task) {
2946 interface Sizable {
2947 fn size(self) -> int
2948 }
2949 struct Box { width: int }
2950 impl Box {
2951 fn size(self) -> string { return "nope" }
2952 }
2953 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2954 measure(Box({width: 3}))
2955}"#,
2956 );
2957 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2958 assert!(
2959 warns[0].contains("method 'size' returns 'string', expected 'int'"),
2960 "unexpected message: {}",
2961 warns[0]
2962 );
2963 }
2964
2965 #[test]
2966 fn test_interface_constraint_param_type_mismatch() {
2967 let warns = iface_warns(
2968 r#"pipeline t(task) {
2969 interface Processor {
2970 fn process(self, x: int) -> string
2971 }
2972 struct MyProc { name: string }
2973 impl MyProc {
2974 fn process(self, x: string) -> string { return x }
2975 }
2976 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
2977 run_proc(MyProc({name: "a"}))
2978}"#,
2979 );
2980 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2981 assert!(
2982 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
2983 "unexpected message: {}",
2984 warns[0]
2985 );
2986 }
2987
2988 #[test]
2989 fn test_interface_constraint_missing_method() {
2990 let warns = iface_warns(
2991 r#"pipeline t(task) {
2992 interface Sizable {
2993 fn size(self) -> int
2994 }
2995 struct Box { width: int }
2996 impl Box {
2997 fn area(self) -> int { return self.width }
2998 }
2999 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3000 measure(Box({width: 3}))
3001}"#,
3002 );
3003 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3004 assert!(
3005 warns[0].contains("missing method 'size'"),
3006 "unexpected message: {}",
3007 warns[0]
3008 );
3009 }
3010
3011 #[test]
3012 fn test_interface_constraint_param_count_mismatch() {
3013 let warns = iface_warns(
3014 r#"pipeline t(task) {
3015 interface Doubler {
3016 fn double(self, x: int) -> int
3017 }
3018 struct Bad { v: int }
3019 impl Bad {
3020 fn double(self) -> int { return self.v * 2 }
3021 }
3022 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3023 run_double(Bad({v: 5}))
3024}"#,
3025 );
3026 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3027 assert!(
3028 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3029 "unexpected message: {}",
3030 warns[0]
3031 );
3032 }
3033
3034 #[test]
3035 fn test_interface_constraint_satisfied() {
3036 let warns = iface_warns(
3037 r#"pipeline t(task) {
3038 interface Sizable {
3039 fn size(self) -> int
3040 }
3041 struct Box { width: int, height: int }
3042 impl Box {
3043 fn size(self) -> int { return self.width * self.height }
3044 }
3045 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3046 measure(Box({width: 3, height: 4}))
3047}"#,
3048 );
3049 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3050 }
3051
3052 #[test]
3053 fn test_interface_constraint_untyped_impl_compatible() {
3054 let warns = iface_warns(
3056 r#"pipeline t(task) {
3057 interface Sizable {
3058 fn size(self) -> int
3059 }
3060 struct Box { width: int }
3061 impl Box {
3062 fn size(self) { return self.width }
3063 }
3064 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3065 measure(Box({width: 3}))
3066}"#,
3067 );
3068 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3069 }
3070
3071 #[test]
3072 fn test_interface_constraint_int_float_covariance() {
3073 let warns = iface_warns(
3075 r#"pipeline t(task) {
3076 interface Measurable {
3077 fn value(self) -> float
3078 }
3079 struct Gauge { v: int }
3080 impl Gauge {
3081 fn value(self) -> int { return self.v }
3082 }
3083 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
3084 read_val(Gauge({v: 42}))
3085}"#,
3086 );
3087 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3088 }
3089
3090 #[test]
3093 fn test_nil_narrowing_then_branch() {
3094 let errs = errors(
3096 r#"pipeline t(task) {
3097 fn greet(name: string | nil) {
3098 if name != nil {
3099 let s: string = name
3100 }
3101 }
3102}"#,
3103 );
3104 assert!(errs.is_empty(), "got: {:?}", errs);
3105 }
3106
3107 #[test]
3108 fn test_nil_narrowing_else_branch() {
3109 let errs = errors(
3111 r#"pipeline t(task) {
3112 fn check(x: string | nil) {
3113 if x != nil {
3114 let s: string = x
3115 } else {
3116 let n: nil = x
3117 }
3118 }
3119}"#,
3120 );
3121 assert!(errs.is_empty(), "got: {:?}", errs);
3122 }
3123
3124 #[test]
3125 fn test_nil_equality_narrows_both() {
3126 let errs = errors(
3128 r#"pipeline t(task) {
3129 fn check(x: string | nil) {
3130 if x == nil {
3131 let n: nil = x
3132 } else {
3133 let s: string = x
3134 }
3135 }
3136}"#,
3137 );
3138 assert!(errs.is_empty(), "got: {:?}", errs);
3139 }
3140
3141 #[test]
3142 fn test_truthiness_narrowing() {
3143 let errs = errors(
3145 r#"pipeline t(task) {
3146 fn check(x: string | nil) {
3147 if x {
3148 let s: string = x
3149 }
3150 }
3151}"#,
3152 );
3153 assert!(errs.is_empty(), "got: {:?}", errs);
3154 }
3155
3156 #[test]
3157 fn test_negation_narrowing() {
3158 let errs = errors(
3160 r#"pipeline t(task) {
3161 fn check(x: string | nil) {
3162 if !x {
3163 let n: nil = x
3164 } else {
3165 let s: string = x
3166 }
3167 }
3168}"#,
3169 );
3170 assert!(errs.is_empty(), "got: {:?}", errs);
3171 }
3172
3173 #[test]
3174 fn test_typeof_narrowing() {
3175 let errs = errors(
3177 r#"pipeline t(task) {
3178 fn check(x: string | int) {
3179 if type_of(x) == "string" {
3180 let s: string = x
3181 }
3182 }
3183}"#,
3184 );
3185 assert!(errs.is_empty(), "got: {:?}", errs);
3186 }
3187
3188 #[test]
3189 fn test_typeof_narrowing_else() {
3190 let errs = errors(
3192 r#"pipeline t(task) {
3193 fn check(x: string | int) {
3194 if type_of(x) == "string" {
3195 let s: string = x
3196 } else {
3197 let i: int = x
3198 }
3199 }
3200}"#,
3201 );
3202 assert!(errs.is_empty(), "got: {:?}", errs);
3203 }
3204
3205 #[test]
3206 fn test_typeof_neq_narrowing() {
3207 let errs = errors(
3209 r#"pipeline t(task) {
3210 fn check(x: string | int) {
3211 if type_of(x) != "string" {
3212 let i: int = x
3213 } else {
3214 let s: string = x
3215 }
3216 }
3217}"#,
3218 );
3219 assert!(errs.is_empty(), "got: {:?}", errs);
3220 }
3221
3222 #[test]
3223 fn test_and_combines_narrowing() {
3224 let errs = errors(
3226 r#"pipeline t(task) {
3227 fn check(x: string | int | nil) {
3228 if x != nil && type_of(x) == "string" {
3229 let s: string = x
3230 }
3231 }
3232}"#,
3233 );
3234 assert!(errs.is_empty(), "got: {:?}", errs);
3235 }
3236
3237 #[test]
3238 fn test_or_falsy_narrowing() {
3239 let errs = errors(
3241 r#"pipeline t(task) {
3242 fn check(x: string | nil, y: int | nil) {
3243 if x || y {
3244 // conservative: can't narrow
3245 } else {
3246 let xn: nil = x
3247 let yn: nil = y
3248 }
3249 }
3250}"#,
3251 );
3252 assert!(errs.is_empty(), "got: {:?}", errs);
3253 }
3254
3255 #[test]
3256 fn test_guard_narrows_outer_scope() {
3257 let errs = errors(
3258 r#"pipeline t(task) {
3259 fn check(x: string | nil) {
3260 guard x != nil else { return }
3261 let s: string = x
3262 }
3263}"#,
3264 );
3265 assert!(errs.is_empty(), "got: {:?}", errs);
3266 }
3267
3268 #[test]
3269 fn test_while_narrows_body() {
3270 let errs = errors(
3271 r#"pipeline t(task) {
3272 fn check(x: string | nil) {
3273 while x != nil {
3274 let s: string = x
3275 break
3276 }
3277 }
3278}"#,
3279 );
3280 assert!(errs.is_empty(), "got: {:?}", errs);
3281 }
3282
3283 #[test]
3284 fn test_early_return_narrows_after_if() {
3285 let errs = errors(
3287 r#"pipeline t(task) {
3288 fn check(x: string | nil) -> string {
3289 if x == nil {
3290 return "default"
3291 }
3292 let s: string = x
3293 return s
3294 }
3295}"#,
3296 );
3297 assert!(errs.is_empty(), "got: {:?}", errs);
3298 }
3299
3300 #[test]
3301 fn test_early_throw_narrows_after_if() {
3302 let errs = errors(
3303 r#"pipeline t(task) {
3304 fn check(x: string | nil) {
3305 if x == nil {
3306 throw "missing"
3307 }
3308 let s: string = x
3309 }
3310}"#,
3311 );
3312 assert!(errs.is_empty(), "got: {:?}", errs);
3313 }
3314
3315 #[test]
3316 fn test_no_narrowing_unknown_type() {
3317 let errs = errors(
3319 r#"pipeline t(task) {
3320 fn check(x) {
3321 if x != nil {
3322 let s: string = x
3323 }
3324 }
3325}"#,
3326 );
3327 assert!(errs.is_empty(), "got: {:?}", errs);
3330 }
3331
3332 #[test]
3333 fn test_reassignment_invalidates_narrowing() {
3334 let errs = errors(
3336 r#"pipeline t(task) {
3337 fn check(x: string | nil) {
3338 var y: string | nil = x
3339 if y != nil {
3340 let s: string = y
3341 y = nil
3342 let s2: string = y
3343 }
3344 }
3345}"#,
3346 );
3347 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3349 assert!(
3350 errs[0].contains("Type mismatch"),
3351 "expected type mismatch, got: {}",
3352 errs[0]
3353 );
3354 }
3355
3356 #[test]
3357 fn test_let_immutable_warning() {
3358 let all = check_source(
3359 r#"pipeline t(task) {
3360 let x = 42
3361 x = 43
3362}"#,
3363 );
3364 let warnings: Vec<_> = all
3365 .iter()
3366 .filter(|d| d.severity == DiagnosticSeverity::Warning)
3367 .collect();
3368 assert!(
3369 warnings.iter().any(|w| w.message.contains("immutable")),
3370 "expected immutability warning, got: {:?}",
3371 warnings
3372 );
3373 }
3374
3375 #[test]
3376 fn test_nested_narrowing() {
3377 let errs = errors(
3378 r#"pipeline t(task) {
3379 fn check(x: string | int | nil) {
3380 if x != nil {
3381 if type_of(x) == "int" {
3382 let i: int = x
3383 }
3384 }
3385 }
3386}"#,
3387 );
3388 assert!(errs.is_empty(), "got: {:?}", errs);
3389 }
3390
3391 #[test]
3392 fn test_match_narrows_arms() {
3393 let errs = errors(
3394 r#"pipeline t(task) {
3395 fn check(x: string | int) {
3396 match x {
3397 "hello" -> {
3398 let s: string = x
3399 }
3400 42 -> {
3401 let i: int = x
3402 }
3403 _ -> {}
3404 }
3405 }
3406}"#,
3407 );
3408 assert!(errs.is_empty(), "got: {:?}", errs);
3409 }
3410
3411 #[test]
3412 fn test_has_narrows_optional_field() {
3413 let errs = errors(
3414 r#"pipeline t(task) {
3415 fn check(x: {name?: string, age: int}) {
3416 if x.has("name") {
3417 let n: {name: string, age: int} = x
3418 }
3419 }
3420}"#,
3421 );
3422 assert!(errs.is_empty(), "got: {:?}", errs);
3423 }
3424}