1use std::collections::BTreeMap;
2
3use crate::ast::*;
4use crate::builtin_signatures;
5use harn_lexer::{FixEdit, Span};
6
7#[derive(Debug, Clone)]
9pub struct InlayHintInfo {
10 pub line: usize,
12 pub column: usize,
13 pub label: String,
15}
16
17#[derive(Debug, Clone)]
19pub struct TypeDiagnostic {
20 pub message: String,
21 pub severity: DiagnosticSeverity,
22 pub span: Option<Span>,
23 pub help: Option<String>,
24 pub fix: Option<Vec<FixEdit>>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DiagnosticSeverity {
30 Error,
31 Warning,
32}
33
34type InferredType = Option<TypeExpr>;
36
37#[derive(Debug, Clone)]
39struct TypeScope {
40 vars: BTreeMap<String, InferredType>,
42 functions: BTreeMap<String, FnSignature>,
44 type_aliases: BTreeMap<String, TypeExpr>,
46 enums: BTreeMap<String, Vec<String>>,
48 interfaces: BTreeMap<String, Vec<InterfaceMethod>>,
50 structs: BTreeMap<String, Vec<(String, InferredType)>>,
52 impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
54 generic_type_params: std::collections::BTreeSet<String>,
56 where_constraints: BTreeMap<String, String>,
59 mutable_vars: std::collections::BTreeSet<String>,
62 narrowed_vars: BTreeMap<String, InferredType>,
65 schema_bindings: BTreeMap<String, InferredType>,
68 parent: Option<Box<TypeScope>>,
69}
70
71#[derive(Debug, Clone)]
73struct ImplMethodSig {
74 name: String,
75 param_count: usize,
77 param_types: Vec<Option<TypeExpr>>,
79 return_type: Option<TypeExpr>,
81}
82
83#[derive(Debug, Clone)]
84struct FnSignature {
85 params: Vec<(String, InferredType)>,
86 return_type: InferredType,
87 type_param_names: Vec<String>,
89 required_params: usize,
91 where_clauses: Vec<(String, String)>,
93 has_rest: bool,
95}
96
97impl TypeScope {
98 fn new() -> 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 schema_bindings: BTreeMap::new(),
112 parent: None,
113 }
114 }
115
116 fn child(&self) -> Self {
117 Self {
118 vars: BTreeMap::new(),
119 functions: BTreeMap::new(),
120 type_aliases: BTreeMap::new(),
121 enums: BTreeMap::new(),
122 interfaces: BTreeMap::new(),
123 structs: BTreeMap::new(),
124 impl_methods: BTreeMap::new(),
125 generic_type_params: std::collections::BTreeSet::new(),
126 where_constraints: BTreeMap::new(),
127 mutable_vars: std::collections::BTreeSet::new(),
128 narrowed_vars: BTreeMap::new(),
129 schema_bindings: BTreeMap::new(),
130 parent: Some(Box::new(self.clone())),
131 }
132 }
133
134 fn get_var(&self, name: &str) -> Option<&InferredType> {
135 self.vars
136 .get(name)
137 .or_else(|| self.parent.as_ref()?.get_var(name))
138 }
139
140 fn get_fn(&self, name: &str) -> Option<&FnSignature> {
141 self.functions
142 .get(name)
143 .or_else(|| self.parent.as_ref()?.get_fn(name))
144 }
145
146 fn get_schema_binding(&self, name: &str) -> Option<&InferredType> {
147 self.schema_bindings
148 .get(name)
149 .or_else(|| self.parent.as_ref()?.get_schema_binding(name))
150 }
151
152 fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
153 self.type_aliases
154 .get(name)
155 .or_else(|| self.parent.as_ref()?.resolve_type(name))
156 }
157
158 fn is_generic_type_param(&self, name: &str) -> bool {
159 self.generic_type_params.contains(name)
160 || self
161 .parent
162 .as_ref()
163 .is_some_and(|p| p.is_generic_type_param(name))
164 }
165
166 fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
167 self.where_constraints
168 .get(type_param)
169 .map(|s| s.as_str())
170 .or_else(|| {
171 self.parent
172 .as_ref()
173 .and_then(|p| p.get_where_constraint(type_param))
174 })
175 }
176
177 fn get_enum(&self, name: &str) -> Option<&Vec<String>> {
178 self.enums
179 .get(name)
180 .or_else(|| self.parent.as_ref()?.get_enum(name))
181 }
182
183 fn get_interface(&self, name: &str) -> Option<&Vec<InterfaceMethod>> {
184 self.interfaces
185 .get(name)
186 .or_else(|| self.parent.as_ref()?.get_interface(name))
187 }
188
189 fn get_struct(&self, name: &str) -> Option<&Vec<(String, InferredType)>> {
190 self.structs
191 .get(name)
192 .or_else(|| self.parent.as_ref()?.get_struct(name))
193 }
194
195 fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
196 self.impl_methods
197 .get(name)
198 .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
199 }
200
201 fn define_var(&mut self, name: &str, ty: InferredType) {
202 self.vars.insert(name.to_string(), ty);
203 }
204
205 fn define_var_mutable(&mut self, name: &str, ty: InferredType) {
206 self.vars.insert(name.to_string(), ty);
207 self.mutable_vars.insert(name.to_string());
208 }
209
210 fn define_schema_binding(&mut self, name: &str, ty: InferredType) {
211 self.schema_bindings.insert(name.to_string(), ty);
212 }
213
214 fn is_mutable(&self, name: &str) -> bool {
216 self.mutable_vars.contains(name) || self.parent.as_ref().is_some_and(|p| p.is_mutable(name))
217 }
218
219 fn define_fn(&mut self, name: &str, sig: FnSignature) {
220 self.functions.insert(name.to_string(), sig);
221 }
222}
223
224#[derive(Debug, Clone, Default)]
227struct Refinements {
228 truthy: Vec<(String, InferredType)>,
230 falsy: Vec<(String, InferredType)>,
232}
233
234impl Refinements {
235 fn empty() -> Self {
236 Self::default()
237 }
238
239 fn inverted(self) -> Self {
241 Self {
242 truthy: self.falsy,
243 falsy: self.truthy,
244 }
245 }
246}
247
248fn builtin_return_type(name: &str) -> InferredType {
251 builtin_signatures::builtin_return_type(name)
252}
253
254fn is_builtin(name: &str) -> bool {
257 builtin_signatures::is_builtin(name)
258}
259
260pub struct TypeChecker {
262 diagnostics: Vec<TypeDiagnostic>,
263 scope: TypeScope,
264 source: Option<String>,
265 hints: Vec<InlayHintInfo>,
266}
267
268impl TypeChecker {
269 pub fn new() -> Self {
270 Self {
271 diagnostics: Vec::new(),
272 scope: TypeScope::new(),
273 source: None,
274 hints: Vec::new(),
275 }
276 }
277
278 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
280 self.source = Some(source.to_string());
281 self.check_inner(program).0
282 }
283
284 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
286 self.check_inner(program).0
287 }
288
289 pub fn check_with_hints(
291 mut self,
292 program: &[SNode],
293 source: &str,
294 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
295 self.source = Some(source.to_string());
296 self.check_inner(program)
297 }
298
299 fn check_inner(mut self, program: &[SNode]) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
300 Self::register_declarations_into(&mut self.scope, program);
302
303 for snode in program {
305 if let Node::Pipeline { body, .. } = &snode.node {
306 Self::register_declarations_into(&mut self.scope, body);
307 }
308 }
309
310 for snode in program {
312 match &snode.node {
313 Node::Pipeline { params, body, .. } => {
314 let mut child = self.scope.child();
315 for p in params {
316 child.define_var(p, None);
317 }
318 self.check_block(body, &mut child);
319 }
320 Node::FnDecl {
321 name,
322 type_params,
323 params,
324 return_type,
325 where_clauses,
326 body,
327 ..
328 } => {
329 let required_params =
330 params.iter().filter(|p| p.default_value.is_none()).count();
331 let sig = FnSignature {
332 params: params
333 .iter()
334 .map(|p| (p.name.clone(), p.type_expr.clone()))
335 .collect(),
336 return_type: return_type.clone(),
337 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
338 required_params,
339 where_clauses: where_clauses
340 .iter()
341 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
342 .collect(),
343 has_rest: params.last().is_some_and(|p| p.rest),
344 };
345 self.scope.define_fn(name, sig);
346 self.check_fn_body(type_params, params, return_type, body, where_clauses);
347 }
348 _ => {
349 let mut scope = self.scope.clone();
350 self.check_node(snode, &mut scope);
351 for (name, ty) in scope.vars {
353 self.scope.vars.entry(name).or_insert(ty);
354 }
355 for name in scope.mutable_vars {
356 self.scope.mutable_vars.insert(name);
357 }
358 }
359 }
360 }
361
362 (self.diagnostics, self.hints)
363 }
364
365 fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
367 for snode in nodes {
368 match &snode.node {
369 Node::TypeDecl { name, type_expr } => {
370 scope.type_aliases.insert(name.clone(), type_expr.clone());
371 }
372 Node::EnumDecl { name, variants, .. } => {
373 let variant_names: Vec<String> =
374 variants.iter().map(|v| v.name.clone()).collect();
375 scope.enums.insert(name.clone(), variant_names);
376 }
377 Node::InterfaceDecl { name, methods, .. } => {
378 scope.interfaces.insert(name.clone(), methods.clone());
379 }
380 Node::StructDecl { name, fields, .. } => {
381 let field_types: Vec<(String, InferredType)> = fields
382 .iter()
383 .map(|f| (f.name.clone(), f.type_expr.clone()))
384 .collect();
385 scope.structs.insert(name.clone(), field_types);
386 }
387 Node::ImplBlock {
388 type_name, methods, ..
389 } => {
390 let sigs: Vec<ImplMethodSig> = methods
391 .iter()
392 .filter_map(|m| {
393 if let Node::FnDecl {
394 name,
395 params,
396 return_type,
397 ..
398 } = &m.node
399 {
400 let non_self: Vec<_> =
401 params.iter().filter(|p| p.name != "self").collect();
402 let param_count = non_self.len();
403 let param_types: Vec<Option<TypeExpr>> =
404 non_self.iter().map(|p| p.type_expr.clone()).collect();
405 Some(ImplMethodSig {
406 name: name.clone(),
407 param_count,
408 param_types,
409 return_type: return_type.clone(),
410 })
411 } else {
412 None
413 }
414 })
415 .collect();
416 scope.impl_methods.insert(type_name.clone(), sigs);
417 }
418 _ => {}
419 }
420 }
421 }
422
423 fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
424 let mut definitely_exited = false;
425 for stmt in stmts {
426 if definitely_exited {
427 self.warning_at("unreachable code".to_string(), stmt.span);
428 break; }
430 self.check_node(stmt, scope);
431 if Self::stmt_definitely_exits(stmt) {
432 definitely_exited = true;
433 }
434 }
435 }
436
437 fn stmt_definitely_exits(stmt: &SNode) -> bool {
439 stmt_definitely_exits(stmt)
440 }
441
442 fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope, mutable: bool) {
444 let define = |scope: &mut TypeScope, name: &str| {
445 if mutable {
446 scope.define_var_mutable(name, None);
447 } else {
448 scope.define_var(name, None);
449 }
450 };
451 match pattern {
452 BindingPattern::Identifier(name) => {
453 define(scope, name);
454 }
455 BindingPattern::Dict(fields) => {
456 for field in fields {
457 let name = field.alias.as_deref().unwrap_or(&field.key);
458 define(scope, name);
459 }
460 }
461 BindingPattern::List(elements) => {
462 for elem in elements {
463 define(scope, &elem.name);
464 }
465 }
466 }
467 }
468
469 fn check_pattern_defaults(&mut self, pattern: &BindingPattern, scope: &mut TypeScope) {
471 match pattern {
472 BindingPattern::Identifier(_) => {}
473 BindingPattern::Dict(fields) => {
474 for field in fields {
475 if let Some(default) = &field.default_value {
476 self.check_binops(default, scope);
477 }
478 }
479 }
480 BindingPattern::List(elements) => {
481 for elem in elements {
482 if let Some(default) = &elem.default_value {
483 self.check_binops(default, scope);
484 }
485 }
486 }
487 }
488 }
489
490 fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
491 let span = snode.span;
492 match &snode.node {
493 Node::LetBinding {
494 pattern,
495 type_ann,
496 value,
497 } => {
498 self.check_binops(value, scope);
499 let inferred = self.infer_type(value, scope);
500 if let BindingPattern::Identifier(name) = pattern {
501 if let Some(expected) = type_ann {
502 if let Some(actual) = &inferred {
503 if !self.types_compatible(expected, actual, scope) {
504 let mut msg = format!(
505 "Type mismatch: '{}' declared as {}, but assigned {}",
506 name,
507 format_type(expected),
508 format_type(actual)
509 );
510 if let Some(detail) = shape_mismatch_detail(expected, actual) {
511 msg.push_str(&format!(" ({})", detail));
512 }
513 self.error_at(msg, span);
514 }
515 }
516 }
517 if type_ann.is_none() {
519 if let Some(ref ty) = inferred {
520 if !is_obvious_type(value, ty) {
521 self.hints.push(InlayHintInfo {
522 line: span.line,
523 column: span.column + "let ".len() + name.len(),
524 label: format!(": {}", format_type(ty)),
525 });
526 }
527 }
528 }
529 let ty = type_ann.clone().or(inferred);
530 scope.define_var(name, ty);
531 scope.define_schema_binding(name, schema_type_expr_from_node(value, scope));
532 } else {
533 self.check_pattern_defaults(pattern, scope);
534 Self::define_pattern_vars(pattern, scope, false);
535 }
536 }
537
538 Node::VarBinding {
539 pattern,
540 type_ann,
541 value,
542 } => {
543 self.check_binops(value, scope);
544 let inferred = self.infer_type(value, scope);
545 if let BindingPattern::Identifier(name) = pattern {
546 if let Some(expected) = type_ann {
547 if let Some(actual) = &inferred {
548 if !self.types_compatible(expected, actual, scope) {
549 let mut msg = format!(
550 "Type mismatch: '{}' declared as {}, but assigned {}",
551 name,
552 format_type(expected),
553 format_type(actual)
554 );
555 if let Some(detail) = shape_mismatch_detail(expected, actual) {
556 msg.push_str(&format!(" ({})", detail));
557 }
558 self.error_at(msg, span);
559 }
560 }
561 }
562 if type_ann.is_none() {
563 if let Some(ref ty) = inferred {
564 if !is_obvious_type(value, ty) {
565 self.hints.push(InlayHintInfo {
566 line: span.line,
567 column: span.column + "var ".len() + name.len(),
568 label: format!(": {}", format_type(ty)),
569 });
570 }
571 }
572 }
573 let ty = type_ann.clone().or(inferred);
574 scope.define_var_mutable(name, ty);
575 scope.define_schema_binding(name, schema_type_expr_from_node(value, scope));
576 } else {
577 self.check_pattern_defaults(pattern, scope);
578 Self::define_pattern_vars(pattern, scope, true);
579 }
580 }
581
582 Node::FnDecl {
583 name,
584 type_params,
585 params,
586 return_type,
587 where_clauses,
588 body,
589 ..
590 } => {
591 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
592 let sig = FnSignature {
593 params: params
594 .iter()
595 .map(|p| (p.name.clone(), p.type_expr.clone()))
596 .collect(),
597 return_type: return_type.clone(),
598 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
599 required_params,
600 where_clauses: where_clauses
601 .iter()
602 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
603 .collect(),
604 has_rest: params.last().is_some_and(|p| p.rest),
605 };
606 scope.define_fn(name, sig.clone());
607 scope.define_var(name, None);
608 self.check_fn_body(type_params, params, return_type, body, where_clauses);
609 }
610
611 Node::ToolDecl {
612 name,
613 params,
614 return_type,
615 body,
616 ..
617 } => {
618 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
620 let sig = FnSignature {
621 params: params
622 .iter()
623 .map(|p| (p.name.clone(), p.type_expr.clone()))
624 .collect(),
625 return_type: return_type.clone(),
626 type_param_names: Vec::new(),
627 required_params,
628 where_clauses: Vec::new(),
629 has_rest: params.last().is_some_and(|p| p.rest),
630 };
631 scope.define_fn(name, sig);
632 scope.define_var(name, None);
633 self.check_fn_body(&[], params, return_type, body, &[]);
634 }
635
636 Node::FunctionCall { name, args } => {
637 self.check_call(name, args, scope, span);
638 }
639
640 Node::IfElse {
641 condition,
642 then_body,
643 else_body,
644 } => {
645 self.check_node(condition, scope);
646 let refs = Self::extract_refinements(condition, scope);
647
648 let mut then_scope = scope.child();
649 apply_refinements(&mut then_scope, &refs.truthy);
650 self.check_block(then_body, &mut then_scope);
651
652 if let Some(else_body) = else_body {
653 let mut else_scope = scope.child();
654 apply_refinements(&mut else_scope, &refs.falsy);
655 self.check_block(else_body, &mut else_scope);
656
657 if Self::block_definitely_exits(then_body)
660 && !Self::block_definitely_exits(else_body)
661 {
662 apply_refinements(scope, &refs.falsy);
663 } else if Self::block_definitely_exits(else_body)
664 && !Self::block_definitely_exits(then_body)
665 {
666 apply_refinements(scope, &refs.truthy);
667 }
668 } else {
669 if Self::block_definitely_exits(then_body) {
671 apply_refinements(scope, &refs.falsy);
672 }
673 }
674 }
675
676 Node::ForIn {
677 pattern,
678 iterable,
679 body,
680 } => {
681 self.check_node(iterable, scope);
682 let mut loop_scope = scope.child();
683 if let BindingPattern::Identifier(variable) = pattern {
684 let elem_type = match self.infer_type(iterable, scope) {
686 Some(TypeExpr::List(inner)) => Some(*inner),
687 Some(TypeExpr::Named(n)) if n == "string" => {
688 Some(TypeExpr::Named("string".into()))
689 }
690 _ => None,
691 };
692 loop_scope.define_var(variable, elem_type);
693 } else {
694 self.check_pattern_defaults(pattern, &mut loop_scope);
695 Self::define_pattern_vars(pattern, &mut loop_scope, false);
696 }
697 self.check_block(body, &mut loop_scope);
698 }
699
700 Node::WhileLoop { condition, body } => {
701 self.check_node(condition, scope);
702 let refs = Self::extract_refinements(condition, scope);
703 let mut loop_scope = scope.child();
704 apply_refinements(&mut loop_scope, &refs.truthy);
705 self.check_block(body, &mut loop_scope);
706 }
707
708 Node::RequireStmt { condition, message } => {
709 self.check_node(condition, scope);
710 if let Some(message) = message {
711 self.check_node(message, scope);
712 }
713 }
714
715 Node::TryCatch {
716 body,
717 error_var,
718 error_type,
719 catch_body,
720 finally_body,
721 ..
722 } => {
723 let mut try_scope = scope.child();
724 self.check_block(body, &mut try_scope);
725 let mut catch_scope = scope.child();
726 if let Some(var) = error_var {
727 catch_scope.define_var(var, error_type.clone());
728 }
729 self.check_block(catch_body, &mut catch_scope);
730 if let Some(fb) = finally_body {
731 let mut finally_scope = scope.child();
732 self.check_block(fb, &mut finally_scope);
733 }
734 }
735
736 Node::TryExpr { body } => {
737 let mut try_scope = scope.child();
738 self.check_block(body, &mut try_scope);
739 }
740
741 Node::ReturnStmt {
742 value: Some(val), ..
743 } => {
744 self.check_node(val, scope);
745 }
746
747 Node::Assignment {
748 target, value, op, ..
749 } => {
750 self.check_node(value, scope);
751 if let Node::Identifier(name) = &target.node {
752 if scope.get_var(name).is_some() && !scope.is_mutable(name) {
754 self.warning_at(
755 format!(
756 "Cannot assign to '{}': variable is immutable (declared with 'let')",
757 name
758 ),
759 span,
760 );
761 }
762
763 if let Some(Some(var_type)) = scope.get_var(name) {
764 let value_type = self.infer_type(value, scope);
765 let assigned = if let Some(op) = op {
766 let var_inferred = scope.get_var(name).cloned().flatten();
767 infer_binary_op_type(op, &var_inferred, &value_type)
768 } else {
769 value_type
770 };
771 if let Some(actual) = &assigned {
772 let check_type = scope
774 .narrowed_vars
775 .get(name)
776 .and_then(|t| t.as_ref())
777 .unwrap_or(var_type);
778 if !self.types_compatible(check_type, actual, scope) {
779 self.error_at(
780 format!(
781 "Type mismatch: cannot assign {} to '{}' (declared as {})",
782 format_type(actual),
783 name,
784 format_type(check_type)
785 ),
786 span,
787 );
788 }
789 }
790 }
791
792 if let Some(original) = scope.narrowed_vars.remove(name) {
794 scope.define_var(name, original);
795 }
796 scope.define_schema_binding(name, None);
797 }
798 }
799
800 Node::TypeDecl { name, type_expr } => {
801 scope.type_aliases.insert(name.clone(), type_expr.clone());
802 }
803
804 Node::EnumDecl { name, variants, .. } => {
805 let variant_names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
806 scope.enums.insert(name.clone(), variant_names);
807 }
808
809 Node::StructDecl { name, fields, .. } => {
810 let field_types: Vec<(String, InferredType)> = fields
811 .iter()
812 .map(|f| (f.name.clone(), f.type_expr.clone()))
813 .collect();
814 scope.structs.insert(name.clone(), field_types);
815 }
816
817 Node::InterfaceDecl { name, methods, .. } => {
818 scope.interfaces.insert(name.clone(), methods.clone());
819 }
820
821 Node::ImplBlock {
822 type_name, methods, ..
823 } => {
824 let sigs: Vec<ImplMethodSig> = methods
826 .iter()
827 .filter_map(|m| {
828 if let Node::FnDecl {
829 name,
830 params,
831 return_type,
832 ..
833 } = &m.node
834 {
835 let non_self: Vec<_> =
836 params.iter().filter(|p| p.name != "self").collect();
837 let param_count = non_self.len();
838 let param_types: Vec<Option<TypeExpr>> =
839 non_self.iter().map(|p| p.type_expr.clone()).collect();
840 Some(ImplMethodSig {
841 name: name.clone(),
842 param_count,
843 param_types,
844 return_type: return_type.clone(),
845 })
846 } else {
847 None
848 }
849 })
850 .collect();
851 scope.impl_methods.insert(type_name.clone(), sigs);
852 for method_sn in methods {
853 self.check_node(method_sn, scope);
854 }
855 }
856
857 Node::TryOperator { operand } => {
858 self.check_node(operand, scope);
859 }
860
861 Node::MatchExpr { value, arms } => {
862 self.check_node(value, scope);
863 let value_type = self.infer_type(value, scope);
864 for arm in arms {
865 self.check_node(&arm.pattern, scope);
866 if let Some(ref vt) = value_type {
868 let value_type_name = format_type(vt);
869 let mismatch = match &arm.pattern.node {
870 Node::StringLiteral(_) => {
871 !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
872 }
873 Node::IntLiteral(_) => {
874 !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
875 && !self.types_compatible(
876 vt,
877 &TypeExpr::Named("float".into()),
878 scope,
879 )
880 }
881 Node::FloatLiteral(_) => {
882 !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
883 && !self.types_compatible(
884 vt,
885 &TypeExpr::Named("int".into()),
886 scope,
887 )
888 }
889 Node::BoolLiteral(_) => {
890 !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
891 }
892 _ => false,
893 };
894 if mismatch {
895 let pattern_type = match &arm.pattern.node {
896 Node::StringLiteral(_) => "string",
897 Node::IntLiteral(_) => "int",
898 Node::FloatLiteral(_) => "float",
899 Node::BoolLiteral(_) => "bool",
900 _ => unreachable!(),
901 };
902 self.warning_at(
903 format!(
904 "Match pattern type mismatch: matching {} against {} literal",
905 value_type_name, pattern_type
906 ),
907 arm.pattern.span,
908 );
909 }
910 }
911 let mut arm_scope = scope.child();
912 if let Node::Identifier(var_name) = &value.node {
914 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(var_name) {
915 let narrowed = match &arm.pattern.node {
916 Node::NilLiteral => narrow_to_single(members, "nil"),
917 Node::StringLiteral(_) => narrow_to_single(members, "string"),
918 Node::IntLiteral(_) => narrow_to_single(members, "int"),
919 Node::FloatLiteral(_) => narrow_to_single(members, "float"),
920 Node::BoolLiteral(_) => narrow_to_single(members, "bool"),
921 _ => None,
922 };
923 if let Some(narrowed_type) = narrowed {
924 arm_scope.define_var(var_name, Some(narrowed_type));
925 }
926 }
927 }
928 self.check_block(&arm.body, &mut arm_scope);
929 }
930 self.check_match_exhaustiveness(value, arms, scope, span);
931 }
932
933 Node::BinaryOp { op, left, right } => {
935 self.check_node(left, scope);
936 self.check_node(right, scope);
937 let lt = self.infer_type(left, scope);
939 let rt = self.infer_type(right, scope);
940 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
941 match op.as_str() {
942 "-" | "/" | "%" => {
943 let numeric = ["int", "float"];
944 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
945 self.error_at(
946 format!(
947 "Operator '{}' requires numeric operands, got {} and {}",
948 op, l, r
949 ),
950 span,
951 );
952 }
953 }
954 "*" => {
955 let numeric = ["int", "float"];
956 let is_numeric =
957 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
958 let is_string_repeat =
959 (l == "string" && r == "int") || (l == "int" && r == "string");
960 if !is_numeric && !is_string_repeat {
961 self.error_at(
962 format!(
963 "Operator '*' requires numeric operands or string * int, got {} and {}",
964 l, r
965 ),
966 span,
967 );
968 }
969 }
970 "+" => {
971 let valid = matches!(
972 (l.as_str(), r.as_str()),
973 ("int" | "float", "int" | "float")
974 | ("string", "string")
975 | ("list", "list")
976 | ("dict", "dict")
977 );
978 if !valid {
979 let msg =
980 format!("Operator '+' is not valid for types {} and {}", l, r);
981 let fix = if l == "string" || r == "string" {
983 self.build_interpolation_fix(left, right, l == "string", span)
984 } else {
985 None
986 };
987 if let Some(fix) = fix {
988 self.error_at_with_fix(msg, span, fix);
989 } else {
990 self.error_at(msg, span);
991 }
992 }
993 }
994 "<" | ">" | "<=" | ">=" => {
995 let comparable = ["int", "float", "string"];
996 if !comparable.contains(&l.as_str())
997 || !comparable.contains(&r.as_str())
998 {
999 self.warning_at(
1000 format!(
1001 "Comparison '{}' may not be meaningful for types {} and {}",
1002 op, l, r
1003 ),
1004 span,
1005 );
1006 } else if (l == "string") != (r == "string") {
1007 self.warning_at(
1008 format!(
1009 "Comparing {} with {} using '{}' may give unexpected results",
1010 l, r, op
1011 ),
1012 span,
1013 );
1014 }
1015 }
1016 _ => {}
1017 }
1018 }
1019 }
1020 Node::UnaryOp { operand, .. } => {
1021 self.check_node(operand, scope);
1022 }
1023 Node::MethodCall {
1024 object,
1025 method,
1026 args,
1027 ..
1028 }
1029 | Node::OptionalMethodCall {
1030 object,
1031 method,
1032 args,
1033 ..
1034 } => {
1035 self.check_node(object, scope);
1036 for arg in args {
1037 self.check_node(arg, scope);
1038 }
1039 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
1043 if scope.is_generic_type_param(&type_name) {
1044 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
1045 if let Some(iface_methods) = scope.get_interface(iface_name) {
1046 let has_method = iface_methods.iter().any(|m| m.name == *method);
1047 if !has_method {
1048 self.warning_at(
1049 format!(
1050 "Method '{}' not found in interface '{}' (constraint on '{}')",
1051 method, iface_name, type_name
1052 ),
1053 span,
1054 );
1055 }
1056 }
1057 }
1058 }
1059 }
1060 }
1061 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1062 self.check_node(object, scope);
1063 }
1064 Node::SubscriptAccess { object, index } => {
1065 self.check_node(object, scope);
1066 self.check_node(index, scope);
1067 }
1068 Node::SliceAccess { object, start, end } => {
1069 self.check_node(object, scope);
1070 if let Some(s) = start {
1071 self.check_node(s, scope);
1072 }
1073 if let Some(e) = end {
1074 self.check_node(e, scope);
1075 }
1076 }
1077
1078 Node::Ternary {
1080 condition,
1081 true_expr,
1082 false_expr,
1083 } => {
1084 self.check_node(condition, scope);
1085 let refs = Self::extract_refinements(condition, scope);
1086
1087 let mut true_scope = scope.child();
1088 apply_refinements(&mut true_scope, &refs.truthy);
1089 self.check_node(true_expr, &mut true_scope);
1090
1091 let mut false_scope = scope.child();
1092 apply_refinements(&mut false_scope, &refs.falsy);
1093 self.check_node(false_expr, &mut false_scope);
1094 }
1095
1096 Node::ThrowStmt { value } => {
1097 self.check_node(value, scope);
1098 }
1099
1100 Node::GuardStmt {
1101 condition,
1102 else_body,
1103 } => {
1104 self.check_node(condition, scope);
1105 let refs = Self::extract_refinements(condition, scope);
1106
1107 let mut else_scope = scope.child();
1108 apply_refinements(&mut else_scope, &refs.falsy);
1109 self.check_block(else_body, &mut else_scope);
1110
1111 apply_refinements(scope, &refs.truthy);
1114 }
1115
1116 Node::SpawnExpr { body } => {
1117 let mut spawn_scope = scope.child();
1118 self.check_block(body, &mut spawn_scope);
1119 }
1120
1121 Node::Parallel {
1122 count,
1123 variable,
1124 body,
1125 } => {
1126 self.check_node(count, scope);
1127 let mut par_scope = scope.child();
1128 if let Some(var) = variable {
1129 par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
1130 }
1131 self.check_block(body, &mut par_scope);
1132 }
1133
1134 Node::ParallelMap {
1135 list,
1136 variable,
1137 body,
1138 }
1139 | Node::ParallelSettle {
1140 list,
1141 variable,
1142 body,
1143 } => {
1144 self.check_node(list, scope);
1145 let mut par_scope = scope.child();
1146 let elem_type = match self.infer_type(list, scope) {
1147 Some(TypeExpr::List(inner)) => Some(*inner),
1148 _ => None,
1149 };
1150 par_scope.define_var(variable, elem_type);
1151 self.check_block(body, &mut par_scope);
1152 }
1153
1154 Node::SelectExpr {
1155 cases,
1156 timeout,
1157 default_body,
1158 } => {
1159 for case in cases {
1160 self.check_node(&case.channel, scope);
1161 let mut case_scope = scope.child();
1162 case_scope.define_var(&case.variable, None);
1163 self.check_block(&case.body, &mut case_scope);
1164 }
1165 if let Some((dur, body)) = timeout {
1166 self.check_node(dur, scope);
1167 let mut timeout_scope = scope.child();
1168 self.check_block(body, &mut timeout_scope);
1169 }
1170 if let Some(body) = default_body {
1171 let mut default_scope = scope.child();
1172 self.check_block(body, &mut default_scope);
1173 }
1174 }
1175
1176 Node::DeadlineBlock { duration, body } => {
1177 self.check_node(duration, scope);
1178 let mut block_scope = scope.child();
1179 self.check_block(body, &mut block_scope);
1180 }
1181
1182 Node::MutexBlock { body } => {
1183 let mut block_scope = scope.child();
1184 self.check_block(body, &mut block_scope);
1185 }
1186
1187 Node::Retry { count, body } => {
1188 self.check_node(count, scope);
1189 let mut retry_scope = scope.child();
1190 self.check_block(body, &mut retry_scope);
1191 }
1192
1193 Node::Closure { params, body, .. } => {
1194 let mut closure_scope = scope.child();
1195 for p in params {
1196 closure_scope.define_var(&p.name, p.type_expr.clone());
1197 }
1198 self.check_block(body, &mut closure_scope);
1199 }
1200
1201 Node::ListLiteral(elements) => {
1202 for elem in elements {
1203 self.check_node(elem, scope);
1204 }
1205 }
1206
1207 Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
1208 for entry in entries {
1209 self.check_node(&entry.key, scope);
1210 self.check_node(&entry.value, scope);
1211 }
1212 }
1213
1214 Node::RangeExpr { start, end, .. } => {
1215 self.check_node(start, scope);
1216 self.check_node(end, scope);
1217 }
1218
1219 Node::Spread(inner) => {
1220 self.check_node(inner, scope);
1221 }
1222
1223 Node::Block(stmts) => {
1224 let mut block_scope = scope.child();
1225 self.check_block(stmts, &mut block_scope);
1226 }
1227
1228 Node::YieldExpr { value } => {
1229 if let Some(v) = value {
1230 self.check_node(v, scope);
1231 }
1232 }
1233
1234 Node::StructConstruct {
1236 struct_name,
1237 fields,
1238 } => {
1239 for entry in fields {
1240 self.check_node(&entry.key, scope);
1241 self.check_node(&entry.value, scope);
1242 }
1243 if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
1244 for entry in fields {
1246 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1247 if !declared_fields.iter().any(|(name, _)| name == key) {
1248 self.warning_at(
1249 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1250 entry.key.span,
1251 );
1252 }
1253 }
1254 }
1255 let provided: Vec<String> = fields
1257 .iter()
1258 .filter_map(|e| match &e.key.node {
1259 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1260 _ => None,
1261 })
1262 .collect();
1263 for (name, _) in &declared_fields {
1264 if !provided.contains(name) {
1265 self.warning_at(
1266 format!(
1267 "Missing field '{}' in struct '{}' construction",
1268 name, struct_name
1269 ),
1270 span,
1271 );
1272 }
1273 }
1274 }
1275 }
1276
1277 Node::EnumConstruct {
1279 enum_name,
1280 variant,
1281 args,
1282 } => {
1283 for arg in args {
1284 self.check_node(arg, scope);
1285 }
1286 if let Some(variants) = scope.get_enum(enum_name) {
1287 if !variants.contains(variant) {
1288 self.warning_at(
1289 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1290 span,
1291 );
1292 }
1293 }
1294 }
1295
1296 Node::InterpolatedString(_) => {}
1298
1299 Node::StringLiteral(_)
1301 | Node::RawStringLiteral(_)
1302 | Node::IntLiteral(_)
1303 | Node::FloatLiteral(_)
1304 | Node::BoolLiteral(_)
1305 | Node::NilLiteral
1306 | Node::Identifier(_)
1307 | Node::DurationLiteral(_)
1308 | Node::BreakStmt
1309 | Node::ContinueStmt
1310 | Node::ReturnStmt { value: None }
1311 | Node::ImportDecl { .. }
1312 | Node::SelectiveImport { .. } => {}
1313
1314 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1317 let mut decl_scope = scope.child();
1318 self.check_block(body, &mut decl_scope);
1319 }
1320 }
1321 }
1322
1323 fn check_fn_body(
1324 &mut self,
1325 type_params: &[TypeParam],
1326 params: &[TypedParam],
1327 return_type: &Option<TypeExpr>,
1328 body: &[SNode],
1329 where_clauses: &[WhereClause],
1330 ) {
1331 let mut fn_scope = self.scope.child();
1332 for tp in type_params {
1335 fn_scope.generic_type_params.insert(tp.name.clone());
1336 }
1337 for wc in where_clauses {
1339 fn_scope
1340 .where_constraints
1341 .insert(wc.type_name.clone(), wc.bound.clone());
1342 }
1343 for param in params {
1344 fn_scope.define_var(¶m.name, param.type_expr.clone());
1345 if let Some(default) = ¶m.default_value {
1346 self.check_node(default, &mut fn_scope);
1347 }
1348 }
1349 let ret_scope_base = if return_type.is_some() {
1352 Some(fn_scope.child())
1353 } else {
1354 None
1355 };
1356
1357 self.check_block(body, &mut fn_scope);
1358
1359 if let Some(ret_type) = return_type {
1361 let mut ret_scope = ret_scope_base.unwrap();
1362 for stmt in body {
1363 self.check_return_type(stmt, ret_type, &mut ret_scope);
1364 }
1365 }
1366 }
1367
1368 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1369 let span = snode.span;
1370 match &snode.node {
1371 Node::ReturnStmt { value: Some(val) } => {
1372 let inferred = self.infer_type(val, scope);
1373 if let Some(actual) = &inferred {
1374 if !self.types_compatible(expected, actual, scope) {
1375 self.error_at(
1376 format!(
1377 "Return type mismatch: expected {}, got {}",
1378 format_type(expected),
1379 format_type(actual)
1380 ),
1381 span,
1382 );
1383 }
1384 }
1385 }
1386 Node::IfElse {
1387 condition,
1388 then_body,
1389 else_body,
1390 } => {
1391 let refs = Self::extract_refinements(condition, scope);
1392 let mut then_scope = scope.child();
1393 apply_refinements(&mut then_scope, &refs.truthy);
1394 for stmt in then_body {
1395 self.check_return_type(stmt, expected, &mut then_scope);
1396 }
1397 if let Some(else_body) = else_body {
1398 let mut else_scope = scope.child();
1399 apply_refinements(&mut else_scope, &refs.falsy);
1400 for stmt in else_body {
1401 self.check_return_type(stmt, expected, &mut else_scope);
1402 }
1403 if Self::block_definitely_exits(then_body)
1405 && !Self::block_definitely_exits(else_body)
1406 {
1407 apply_refinements(scope, &refs.falsy);
1408 } else if Self::block_definitely_exits(else_body)
1409 && !Self::block_definitely_exits(then_body)
1410 {
1411 apply_refinements(scope, &refs.truthy);
1412 }
1413 } else {
1414 if Self::block_definitely_exits(then_body) {
1416 apply_refinements(scope, &refs.falsy);
1417 }
1418 }
1419 }
1420 _ => {}
1421 }
1422 }
1423
1424 fn satisfies_interface(
1430 &self,
1431 type_name: &str,
1432 interface_name: &str,
1433 scope: &TypeScope,
1434 ) -> bool {
1435 self.interface_mismatch_reason(type_name, interface_name, scope)
1436 .is_none()
1437 }
1438
1439 fn interface_mismatch_reason(
1442 &self,
1443 type_name: &str,
1444 interface_name: &str,
1445 scope: &TypeScope,
1446 ) -> Option<String> {
1447 let interface_methods = match scope.get_interface(interface_name) {
1448 Some(methods) => methods,
1449 None => return Some(format!("interface '{}' not found", interface_name)),
1450 };
1451 let impl_methods = match scope.get_impl_methods(type_name) {
1452 Some(methods) => methods,
1453 None => {
1454 if interface_methods.is_empty() {
1455 return None;
1456 }
1457 let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1458 return Some(format!("missing method(s): {}", names.join(", ")));
1459 }
1460 };
1461 for iface_method in interface_methods {
1462 let iface_params: Vec<_> = iface_method
1463 .params
1464 .iter()
1465 .filter(|p| p.name != "self")
1466 .collect();
1467 let iface_param_count = iface_params.len();
1468 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1469 let impl_method = match matching_impl {
1470 Some(m) => m,
1471 None => {
1472 return Some(format!("missing method '{}'", iface_method.name));
1473 }
1474 };
1475 if impl_method.param_count != iface_param_count {
1476 return Some(format!(
1477 "method '{}' has {} parameter(s), expected {}",
1478 iface_method.name, impl_method.param_count, iface_param_count
1479 ));
1480 }
1481 for (i, iface_param) in iface_params.iter().enumerate() {
1483 if let (Some(expected), Some(actual)) = (
1484 &iface_param.type_expr,
1485 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1486 ) {
1487 if !self.types_compatible(expected, actual, scope) {
1488 return Some(format!(
1489 "method '{}' parameter {} has type '{}', expected '{}'",
1490 iface_method.name,
1491 i + 1,
1492 format_type(actual),
1493 format_type(expected),
1494 ));
1495 }
1496 }
1497 }
1498 if let (Some(expected_ret), Some(actual_ret)) =
1500 (&iface_method.return_type, &impl_method.return_type)
1501 {
1502 if !self.types_compatible(expected_ret, actual_ret, scope) {
1503 return Some(format!(
1504 "method '{}' returns '{}', expected '{}'",
1505 iface_method.name,
1506 format_type(actual_ret),
1507 format_type(expected_ret),
1508 ));
1509 }
1510 }
1511 }
1512 None
1513 }
1514
1515 fn bind_type_param(
1516 param_name: &str,
1517 concrete: &TypeExpr,
1518 bindings: &mut BTreeMap<String, TypeExpr>,
1519 ) -> Result<(), String> {
1520 if let Some(existing) = bindings.get(param_name) {
1521 if existing != concrete {
1522 return Err(format!(
1523 "type parameter '{}' was inferred as both {} and {}",
1524 param_name,
1525 format_type(existing),
1526 format_type(concrete)
1527 ));
1528 }
1529 return Ok(());
1530 }
1531 bindings.insert(param_name.to_string(), concrete.clone());
1532 Ok(())
1533 }
1534
1535 fn extract_type_bindings(
1538 param_type: &TypeExpr,
1539 arg_type: &TypeExpr,
1540 type_params: &std::collections::BTreeSet<String>,
1541 bindings: &mut BTreeMap<String, TypeExpr>,
1542 ) -> Result<(), String> {
1543 match (param_type, arg_type) {
1544 (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
1545 Self::bind_type_param(param_name, concrete, bindings)
1546 }
1547 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1548 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
1549 }
1550 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1551 Self::extract_type_bindings(pk, ak, type_params, bindings)?;
1552 Self::extract_type_bindings(pv, av, type_params, bindings)
1553 }
1554 (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
1555 for param_field in param_fields {
1556 if let Some(arg_field) = arg_fields
1557 .iter()
1558 .find(|field| field.name == param_field.name)
1559 {
1560 Self::extract_type_bindings(
1561 ¶m_field.type_expr,
1562 &arg_field.type_expr,
1563 type_params,
1564 bindings,
1565 )?;
1566 }
1567 }
1568 Ok(())
1569 }
1570 (
1571 TypeExpr::FnType {
1572 params: p_params,
1573 return_type: p_ret,
1574 },
1575 TypeExpr::FnType {
1576 params: a_params,
1577 return_type: a_ret,
1578 },
1579 ) => {
1580 for (param, arg) in p_params.iter().zip(a_params.iter()) {
1581 Self::extract_type_bindings(param, arg, type_params, bindings)?;
1582 }
1583 Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
1584 }
1585 _ => Ok(()),
1586 }
1587 }
1588
1589 fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
1590 match ty {
1591 TypeExpr::Named(name) => bindings
1592 .get(name)
1593 .cloned()
1594 .unwrap_or_else(|| TypeExpr::Named(name.clone())),
1595 TypeExpr::Union(items) => TypeExpr::Union(
1596 items
1597 .iter()
1598 .map(|item| Self::apply_type_bindings(item, bindings))
1599 .collect(),
1600 ),
1601 TypeExpr::Shape(fields) => TypeExpr::Shape(
1602 fields
1603 .iter()
1604 .map(|field| ShapeField {
1605 name: field.name.clone(),
1606 type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
1607 optional: field.optional,
1608 })
1609 .collect(),
1610 ),
1611 TypeExpr::List(inner) => {
1612 TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
1613 }
1614 TypeExpr::DictType(key, value) => TypeExpr::DictType(
1615 Box::new(Self::apply_type_bindings(key, bindings)),
1616 Box::new(Self::apply_type_bindings(value, bindings)),
1617 ),
1618 TypeExpr::FnType {
1619 params,
1620 return_type,
1621 } => TypeExpr::FnType {
1622 params: params
1623 .iter()
1624 .map(|param| Self::apply_type_bindings(param, bindings))
1625 .collect(),
1626 return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
1627 },
1628 TypeExpr::Never => TypeExpr::Never,
1629 }
1630 }
1631
1632 fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
1633 let mut inferred: Option<TypeExpr> = None;
1634 for item in items {
1635 let Some(item_type) = self.infer_type(item, scope) else {
1636 return TypeExpr::Named("list".into());
1637 };
1638 inferred = Some(match inferred {
1639 None => item_type,
1640 Some(current) if current == item_type => current,
1641 Some(TypeExpr::Union(mut members)) => {
1642 if !members.contains(&item_type) {
1643 members.push(item_type);
1644 }
1645 TypeExpr::Union(members)
1646 }
1647 Some(current) => TypeExpr::Union(vec![current, item_type]),
1648 });
1649 }
1650 inferred
1651 .map(|item_type| TypeExpr::List(Box::new(item_type)))
1652 .unwrap_or_else(|| TypeExpr::Named("list".into()))
1653 }
1654
1655 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1657 match &condition.node {
1658 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1660 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1661 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1662 return nil_ref;
1663 }
1664 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1665 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1666 return typeof_ref;
1667 }
1668 Refinements::empty()
1669 }
1670
1671 Node::BinaryOp { op, left, right } if op == "&&" => {
1673 let left_ref = Self::extract_refinements(left, scope);
1674 let right_ref = Self::extract_refinements(right, scope);
1675 let mut truthy = left_ref.truthy;
1676 truthy.extend(right_ref.truthy);
1677 Refinements {
1678 truthy,
1679 falsy: vec![],
1680 }
1681 }
1682
1683 Node::BinaryOp { op, left, right } if op == "||" => {
1685 let left_ref = Self::extract_refinements(left, scope);
1686 let right_ref = Self::extract_refinements(right, scope);
1687 let mut falsy = left_ref.falsy;
1688 falsy.extend(right_ref.falsy);
1689 Refinements {
1690 truthy: vec![],
1691 falsy,
1692 }
1693 }
1694
1695 Node::UnaryOp { op, operand } if op == "!" => {
1697 Self::extract_refinements(operand, scope).inverted()
1698 }
1699
1700 Node::Identifier(name) => {
1702 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1703 if members
1704 .iter()
1705 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1706 {
1707 if let Some(narrowed) = remove_from_union(members, "nil") {
1708 return Refinements {
1709 truthy: vec![(name.clone(), Some(narrowed))],
1710 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1711 };
1712 }
1713 }
1714 }
1715 Refinements::empty()
1716 }
1717
1718 Node::MethodCall {
1720 object,
1721 method,
1722 args,
1723 } if method == "has" && args.len() == 1 => {
1724 Self::extract_has_refinements(object, args, scope)
1725 }
1726
1727 Node::FunctionCall { name, args }
1728 if (name == "schema_is" || name == "is_type") && args.len() == 2 =>
1729 {
1730 Self::extract_schema_refinements(args, scope)
1731 }
1732
1733 _ => Refinements::empty(),
1734 }
1735 }
1736
1737 fn extract_nil_refinements(
1739 op: &str,
1740 left: &SNode,
1741 right: &SNode,
1742 scope: &TypeScope,
1743 ) -> Refinements {
1744 let var_node = if matches!(right.node, Node::NilLiteral) {
1745 left
1746 } else if matches!(left.node, Node::NilLiteral) {
1747 right
1748 } else {
1749 return Refinements::empty();
1750 };
1751
1752 if let Node::Identifier(name) = &var_node.node {
1753 let var_type = scope.get_var(name).cloned().flatten();
1754 match var_type {
1755 Some(TypeExpr::Union(ref members)) => {
1756 if let Some(narrowed) = remove_from_union(members, "nil") {
1757 let neq_refs = Refinements {
1758 truthy: vec![(name.clone(), Some(narrowed))],
1759 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1760 };
1761 return if op == "!=" {
1762 neq_refs
1763 } else {
1764 neq_refs.inverted()
1765 };
1766 }
1767 }
1768 Some(TypeExpr::Named(ref n)) if n == "nil" => {
1769 let eq_refs = Refinements {
1771 truthy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1772 falsy: vec![(name.clone(), Some(TypeExpr::Never))],
1773 };
1774 return if op == "==" {
1775 eq_refs
1776 } else {
1777 eq_refs.inverted()
1778 };
1779 }
1780 _ => {}
1781 }
1782 }
1783 Refinements::empty()
1784 }
1785
1786 fn extract_typeof_refinements(
1788 op: &str,
1789 left: &SNode,
1790 right: &SNode,
1791 scope: &TypeScope,
1792 ) -> Refinements {
1793 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1794 (extract_type_of_var(left), &right.node)
1795 {
1796 (var, tn.clone())
1797 } else if let (Node::StringLiteral(tn), Some(var)) =
1798 (&left.node, extract_type_of_var(right))
1799 {
1800 (var, tn.clone())
1801 } else {
1802 return Refinements::empty();
1803 };
1804
1805 const KNOWN_TYPES: &[&str] = &[
1806 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1807 ];
1808 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1809 return Refinements::empty();
1810 }
1811
1812 let var_type = scope.get_var(&var_name).cloned().flatten();
1813 match var_type {
1814 Some(TypeExpr::Union(ref members)) => {
1815 let narrowed = narrow_to_single(members, &type_name);
1816 let remaining = remove_from_union(members, &type_name);
1817 if narrowed.is_some() || remaining.is_some() {
1818 let eq_refs = Refinements {
1819 truthy: narrowed
1820 .map(|n| vec![(var_name.clone(), Some(n))])
1821 .unwrap_or_default(),
1822 falsy: remaining
1823 .map(|r| vec![(var_name.clone(), Some(r))])
1824 .unwrap_or_default(),
1825 };
1826 return if op == "==" {
1827 eq_refs
1828 } else {
1829 eq_refs.inverted()
1830 };
1831 }
1832 }
1833 Some(TypeExpr::Named(ref n)) if n == &type_name => {
1834 let eq_refs = Refinements {
1837 truthy: vec![(var_name.clone(), Some(TypeExpr::Named(type_name)))],
1838 falsy: vec![(var_name.clone(), Some(TypeExpr::Never))],
1839 };
1840 return if op == "==" {
1841 eq_refs
1842 } else {
1843 eq_refs.inverted()
1844 };
1845 }
1846 _ => {}
1847 }
1848 Refinements::empty()
1849 }
1850
1851 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1853 if let Node::Identifier(var_name) = &object.node {
1854 if let Node::StringLiteral(key) = &args[0].node {
1855 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1856 if fields.iter().any(|f| f.name == *key && f.optional) {
1857 let narrowed_fields: Vec<ShapeField> = fields
1858 .iter()
1859 .map(|f| {
1860 if f.name == *key {
1861 ShapeField {
1862 name: f.name.clone(),
1863 type_expr: f.type_expr.clone(),
1864 optional: false,
1865 }
1866 } else {
1867 f.clone()
1868 }
1869 })
1870 .collect();
1871 return Refinements {
1872 truthy: vec![(
1873 var_name.clone(),
1874 Some(TypeExpr::Shape(narrowed_fields)),
1875 )],
1876 falsy: vec![],
1877 };
1878 }
1879 }
1880 }
1881 }
1882 Refinements::empty()
1883 }
1884
1885 fn extract_schema_refinements(args: &[SNode], scope: &TypeScope) -> Refinements {
1886 let Node::Identifier(var_name) = &args[0].node else {
1887 return Refinements::empty();
1888 };
1889 let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) else {
1890 return Refinements::empty();
1891 };
1892 let Some(Some(var_type)) = scope.get_var(var_name).cloned() else {
1893 return Refinements::empty();
1894 };
1895
1896 let truthy = intersect_types(&var_type, &schema_type)
1897 .map(|ty| vec![(var_name.clone(), Some(ty))])
1898 .unwrap_or_default();
1899 let falsy = subtract_type(&var_type, &schema_type)
1900 .map(|ty| vec![(var_name.clone(), Some(ty))])
1901 .unwrap_or_default();
1902
1903 Refinements { truthy, falsy }
1904 }
1905
1906 fn block_definitely_exits(stmts: &[SNode]) -> bool {
1908 block_definitely_exits(stmts)
1909 }
1910
1911 fn check_match_exhaustiveness(
1912 &mut self,
1913 value: &SNode,
1914 arms: &[MatchArm],
1915 scope: &TypeScope,
1916 span: Span,
1917 ) {
1918 let enum_name = match &value.node {
1920 Node::PropertyAccess { object, property } if property == "variant" => {
1921 match self.infer_type(object, scope) {
1923 Some(TypeExpr::Named(name)) => {
1924 if scope.get_enum(&name).is_some() {
1925 Some(name)
1926 } else {
1927 None
1928 }
1929 }
1930 _ => None,
1931 }
1932 }
1933 _ => {
1934 match self.infer_type(value, scope) {
1936 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1937 _ => None,
1938 }
1939 }
1940 };
1941
1942 let Some(enum_name) = enum_name else {
1943 self.check_match_exhaustiveness_union(value, arms, scope, span);
1945 return;
1946 };
1947 let Some(variants) = scope.get_enum(&enum_name) else {
1948 return;
1949 };
1950
1951 let mut covered: Vec<String> = Vec::new();
1953 let mut has_wildcard = false;
1954
1955 for arm in arms {
1956 match &arm.pattern.node {
1957 Node::StringLiteral(s) => covered.push(s.clone()),
1959 Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1961 has_wildcard = true;
1962 }
1963 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1965 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1967 _ => {
1968 has_wildcard = true;
1970 }
1971 }
1972 }
1973
1974 if has_wildcard {
1975 return;
1976 }
1977
1978 let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1979 if !missing.is_empty() {
1980 let missing_str = missing
1981 .iter()
1982 .map(|s| format!("\"{}\"", s))
1983 .collect::<Vec<_>>()
1984 .join(", ");
1985 self.warning_at(
1986 format!(
1987 "Non-exhaustive match on enum {}: missing variants {}",
1988 enum_name, missing_str
1989 ),
1990 span,
1991 );
1992 }
1993 }
1994
1995 fn check_match_exhaustiveness_union(
1997 &mut self,
1998 value: &SNode,
1999 arms: &[MatchArm],
2000 scope: &TypeScope,
2001 span: Span,
2002 ) {
2003 let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
2004 return;
2005 };
2006 if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
2008 return;
2009 }
2010
2011 let mut has_wildcard = false;
2012 let mut covered_types: Vec<String> = Vec::new();
2013
2014 for arm in arms {
2015 match &arm.pattern.node {
2016 Node::NilLiteral => covered_types.push("nil".into()),
2019 Node::BoolLiteral(_) => {
2020 if !covered_types.contains(&"bool".into()) {
2021 covered_types.push("bool".into());
2022 }
2023 }
2024 Node::IntLiteral(_) => {
2025 if !covered_types.contains(&"int".into()) {
2026 covered_types.push("int".into());
2027 }
2028 }
2029 Node::FloatLiteral(_) => {
2030 if !covered_types.contains(&"float".into()) {
2031 covered_types.push("float".into());
2032 }
2033 }
2034 Node::StringLiteral(_) => {
2035 if !covered_types.contains(&"string".into()) {
2036 covered_types.push("string".into());
2037 }
2038 }
2039 Node::Identifier(name) if name == "_" => {
2040 has_wildcard = true;
2041 }
2042 _ => {
2043 has_wildcard = true;
2044 }
2045 }
2046 }
2047
2048 if has_wildcard {
2049 return;
2050 }
2051
2052 let type_names: Vec<&str> = members
2053 .iter()
2054 .filter_map(|m| match m {
2055 TypeExpr::Named(n) => Some(n.as_str()),
2056 _ => None,
2057 })
2058 .collect();
2059 let missing: Vec<&&str> = type_names
2060 .iter()
2061 .filter(|t| !covered_types.iter().any(|c| c == **t))
2062 .collect();
2063 if !missing.is_empty() {
2064 let missing_str = missing
2065 .iter()
2066 .map(|s| s.to_string())
2067 .collect::<Vec<_>>()
2068 .join(", ");
2069 self.warning_at(
2070 format!(
2071 "Non-exhaustive match on union type: missing {}",
2072 missing_str
2073 ),
2074 span,
2075 );
2076 }
2077 }
2078
2079 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
2080 if name == "unreachable" {
2083 if let Some(arg) = args.first() {
2084 if matches!(&arg.node, Node::Identifier(_)) {
2085 let arg_type = self.infer_type(arg, scope);
2086 if let Some(ref ty) = arg_type {
2087 if !matches!(ty, TypeExpr::Never) {
2088 self.error_at(
2089 format!(
2090 "unreachable() argument has type `{}` — not all cases are handled",
2091 format_type(ty)
2092 ),
2093 span,
2094 );
2095 }
2096 }
2097 }
2098 }
2099 for arg in args {
2100 self.check_node(arg, scope);
2101 }
2102 return;
2103 }
2104
2105 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
2107 if let Some(sig) = scope.get_fn(name).cloned() {
2108 if !has_spread
2109 && !is_builtin(name)
2110 && !sig.has_rest
2111 && (args.len() < sig.required_params || args.len() > sig.params.len())
2112 {
2113 let expected = if sig.required_params == sig.params.len() {
2114 format!("{}", sig.params.len())
2115 } else {
2116 format!("{}-{}", sig.required_params, sig.params.len())
2117 };
2118 self.warning_at(
2119 format!(
2120 "Function '{}' expects {} arguments, got {}",
2121 name,
2122 expected,
2123 args.len()
2124 ),
2125 span,
2126 );
2127 }
2128 let call_scope = if sig.type_param_names.is_empty() {
2131 scope.clone()
2132 } else {
2133 let mut s = scope.child();
2134 for tp_name in &sig.type_param_names {
2135 s.generic_type_params.insert(tp_name.clone());
2136 }
2137 s
2138 };
2139 let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
2140 let type_param_set: std::collections::BTreeSet<String> =
2141 sig.type_param_names.iter().cloned().collect();
2142 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2143 if let Some(param_ty) = param_type {
2144 if let Some(arg_ty) = self.infer_type(arg, scope) {
2145 if let Err(message) = Self::extract_type_bindings(
2146 param_ty,
2147 &arg_ty,
2148 &type_param_set,
2149 &mut type_bindings,
2150 ) {
2151 self.error_at(message, arg.span);
2152 }
2153 }
2154 }
2155 }
2156 for (i, (arg, (param_name, param_type))) in
2157 args.iter().zip(sig.params.iter()).enumerate()
2158 {
2159 if let Some(expected) = param_type {
2160 let actual = self.infer_type(arg, scope);
2161 if let Some(actual) = &actual {
2162 let expected = Self::apply_type_bindings(expected, &type_bindings);
2163 if !self.types_compatible(&expected, actual, &call_scope) {
2164 self.error_at(
2165 format!(
2166 "Argument {} ('{}'): expected {}, got {}",
2167 i + 1,
2168 param_name,
2169 format_type(&expected),
2170 format_type(actual)
2171 ),
2172 arg.span,
2173 );
2174 }
2175 }
2176 }
2177 }
2178 if !sig.where_clauses.is_empty() {
2179 for (type_param, bound) in &sig.where_clauses {
2180 if let Some(concrete_type) = type_bindings.get(type_param) {
2181 let concrete_name = format_type(concrete_type);
2182 if let Some(reason) =
2183 self.interface_mismatch_reason(&concrete_name, bound, scope)
2184 {
2185 self.error_at(
2186 format!(
2187 "Type '{}' does not satisfy interface '{}': {} \
2188 (required by constraint `where {}: {}`)",
2189 concrete_name, bound, reason, type_param, bound
2190 ),
2191 span,
2192 );
2193 }
2194 }
2195 }
2196 }
2197 }
2198 for arg in args {
2200 self.check_node(arg, scope);
2201 }
2202 }
2203
2204 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
2206 match &snode.node {
2207 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
2208 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
2209 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
2210 Some(TypeExpr::Named("string".into()))
2211 }
2212 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
2213 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
2214 Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
2215 Node::DictLiteral(entries) => {
2216 let mut fields = Vec::new();
2218 for entry in entries {
2219 let key = match &entry.key.node {
2220 Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
2221 _ => return Some(TypeExpr::Named("dict".into())),
2222 };
2223 let val_type = self
2224 .infer_type(&entry.value, scope)
2225 .unwrap_or(TypeExpr::Named("nil".into()));
2226 fields.push(ShapeField {
2227 name: key,
2228 type_expr: val_type,
2229 optional: false,
2230 });
2231 }
2232 if !fields.is_empty() {
2233 Some(TypeExpr::Shape(fields))
2234 } else {
2235 Some(TypeExpr::Named("dict".into()))
2236 }
2237 }
2238 Node::Closure { params, body, .. } => {
2239 let all_typed = params.iter().all(|p| p.type_expr.is_some());
2241 if all_typed && !params.is_empty() {
2242 let param_types: Vec<TypeExpr> =
2243 params.iter().filter_map(|p| p.type_expr.clone()).collect();
2244 let ret = body.last().and_then(|last| self.infer_type(last, scope));
2246 if let Some(ret_type) = ret {
2247 return Some(TypeExpr::FnType {
2248 params: param_types,
2249 return_type: Box::new(ret_type),
2250 });
2251 }
2252 }
2253 Some(TypeExpr::Named("closure".into()))
2254 }
2255
2256 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
2257
2258 Node::FunctionCall { name, args } => {
2259 if scope.get_struct(name).is_some() {
2261 return Some(TypeExpr::Named(name.clone()));
2262 }
2263 if let Some(sig) = scope.get_fn(name) {
2265 let mut return_type = sig.return_type.clone();
2266 if let Some(ty) = return_type.take() {
2267 if sig.type_param_names.is_empty() {
2268 return Some(ty);
2269 }
2270 let mut bindings = BTreeMap::new();
2271 let type_param_set: std::collections::BTreeSet<String> =
2272 sig.type_param_names.iter().cloned().collect();
2273 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2274 if let Some(param_ty) = param_type {
2275 if let Some(arg_ty) = self.infer_type(arg, scope) {
2276 let _ = Self::extract_type_bindings(
2277 param_ty,
2278 &arg_ty,
2279 &type_param_set,
2280 &mut bindings,
2281 );
2282 }
2283 }
2284 }
2285 return Some(Self::apply_type_bindings(&ty, &bindings));
2286 }
2287 return None;
2288 }
2289 builtin_return_type(name)
2291 }
2292
2293 Node::BinaryOp { op, left, right } => {
2294 let lt = self.infer_type(left, scope);
2295 let rt = self.infer_type(right, scope);
2296 infer_binary_op_type(op, <, &rt)
2297 }
2298
2299 Node::UnaryOp { op, operand } => {
2300 let t = self.infer_type(operand, scope);
2301 match op.as_str() {
2302 "!" => Some(TypeExpr::Named("bool".into())),
2303 "-" => t, _ => None,
2305 }
2306 }
2307
2308 Node::Ternary {
2309 condition,
2310 true_expr,
2311 false_expr,
2312 } => {
2313 let refs = Self::extract_refinements(condition, scope);
2314
2315 let mut true_scope = scope.child();
2316 apply_refinements(&mut true_scope, &refs.truthy);
2317 let tt = self.infer_type(true_expr, &true_scope);
2318
2319 let mut false_scope = scope.child();
2320 apply_refinements(&mut false_scope, &refs.falsy);
2321 let ft = self.infer_type(false_expr, &false_scope);
2322
2323 match (&tt, &ft) {
2324 (Some(a), Some(b)) if a == b => tt,
2325 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
2326 (Some(_), None) => tt,
2327 (None, Some(_)) => ft,
2328 (None, None) => None,
2329 }
2330 }
2331
2332 Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
2333
2334 Node::PropertyAccess { object, property } => {
2335 if let Node::Identifier(name) = &object.node {
2337 if scope.get_enum(name).is_some() {
2338 return Some(TypeExpr::Named(name.clone()));
2339 }
2340 }
2341 if property == "variant" {
2343 let obj_type = self.infer_type(object, scope);
2344 if let Some(TypeExpr::Named(name)) = &obj_type {
2345 if scope.get_enum(name).is_some() {
2346 return Some(TypeExpr::Named("string".into()));
2347 }
2348 }
2349 }
2350 let obj_type = self.infer_type(object, scope);
2352 if let Some(TypeExpr::Shape(fields)) = &obj_type {
2353 if let Some(field) = fields.iter().find(|f| f.name == *property) {
2354 return Some(field.type_expr.clone());
2355 }
2356 }
2357 None
2358 }
2359
2360 Node::SubscriptAccess { object, index } => {
2361 let obj_type = self.infer_type(object, scope);
2362 match &obj_type {
2363 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
2364 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
2365 Some(TypeExpr::Shape(fields)) => {
2366 if let Node::StringLiteral(key) = &index.node {
2368 fields
2369 .iter()
2370 .find(|f| &f.name == key)
2371 .map(|f| f.type_expr.clone())
2372 } else {
2373 None
2374 }
2375 }
2376 Some(TypeExpr::Named(n)) if n == "list" => None,
2377 Some(TypeExpr::Named(n)) if n == "dict" => None,
2378 Some(TypeExpr::Named(n)) if n == "string" => {
2379 Some(TypeExpr::Named("string".into()))
2380 }
2381 _ => None,
2382 }
2383 }
2384 Node::SliceAccess { object, .. } => {
2385 let obj_type = self.infer_type(object, scope);
2387 match &obj_type {
2388 Some(TypeExpr::List(_)) => obj_type,
2389 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
2390 Some(TypeExpr::Named(n)) if n == "string" => {
2391 Some(TypeExpr::Named("string".into()))
2392 }
2393 _ => None,
2394 }
2395 }
2396 Node::MethodCall { object, method, .. }
2397 | Node::OptionalMethodCall { object, method, .. } => {
2398 let obj_type = self.infer_type(object, scope);
2399 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
2400 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
2401 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
2402 match method.as_str() {
2403 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
2405 Some(TypeExpr::Named("bool".into()))
2406 }
2407 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
2409 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
2411 | "pad_left" | "pad_right" | "repeat" | "join" => {
2412 Some(TypeExpr::Named("string".into()))
2413 }
2414 "split" | "chars" => Some(TypeExpr::Named("list".into())),
2415 "filter" => {
2417 if is_dict {
2418 Some(TypeExpr::Named("dict".into()))
2419 } else {
2420 Some(TypeExpr::Named("list".into()))
2421 }
2422 }
2423 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2425 "reduce" | "find" | "first" | "last" => None,
2426 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2428 "merge" | "map_values" | "rekey" | "map_keys" => {
2429 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2433 Some(TypeExpr::DictType(
2434 Box::new(TypeExpr::Named("string".into())),
2435 v.clone(),
2436 ))
2437 } else {
2438 Some(TypeExpr::Named("dict".into()))
2439 }
2440 }
2441 "to_string" => Some(TypeExpr::Named("string".into())),
2443 "to_int" => Some(TypeExpr::Named("int".into())),
2444 "to_float" => Some(TypeExpr::Named("float".into())),
2445 _ => None,
2446 }
2447 }
2448
2449 Node::TryOperator { operand } => {
2451 match self.infer_type(operand, scope) {
2452 Some(TypeExpr::Named(name)) if name == "Result" => None, _ => None,
2454 }
2455 }
2456
2457 Node::ThrowStmt { .. }
2459 | Node::ReturnStmt { .. }
2460 | Node::BreakStmt
2461 | Node::ContinueStmt => Some(TypeExpr::Never),
2462
2463 Node::IfElse {
2465 then_body,
2466 else_body,
2467 ..
2468 } => {
2469 let then_type = self.infer_block_type(then_body, scope);
2470 let else_type = else_body
2471 .as_ref()
2472 .and_then(|eb| self.infer_block_type(eb, scope));
2473 match (then_type, else_type) {
2474 (Some(TypeExpr::Never), Some(TypeExpr::Never)) => Some(TypeExpr::Never),
2475 (Some(TypeExpr::Never), Some(other)) | (Some(other), Some(TypeExpr::Never)) => {
2476 Some(other)
2477 }
2478 (Some(t), Some(e)) if t == e => Some(t),
2479 (Some(t), Some(e)) => Some(simplify_union(vec![t, e])),
2480 (Some(t), None) => Some(t),
2481 (None, _) => None,
2482 }
2483 }
2484
2485 _ => None,
2486 }
2487 }
2488
2489 fn infer_block_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
2491 if Self::block_definitely_exits(stmts) {
2492 return Some(TypeExpr::Never);
2493 }
2494 stmts.last().and_then(|s| self.infer_type(s, scope))
2495 }
2496
2497 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2499 if let TypeExpr::Named(name) = expected {
2501 if scope.is_generic_type_param(name) {
2502 return true;
2503 }
2504 }
2505 if let TypeExpr::Named(name) = actual {
2506 if scope.is_generic_type_param(name) {
2507 return true;
2508 }
2509 }
2510 let expected = self.resolve_alias(expected, scope);
2511 let actual = self.resolve_alias(actual, scope);
2512
2513 if let TypeExpr::Named(iface_name) = &expected {
2516 if scope.get_interface(iface_name).is_some() {
2517 if let TypeExpr::Named(type_name) = &actual {
2518 return self.satisfies_interface(type_name, iface_name, scope);
2519 }
2520 return false;
2521 }
2522 }
2523
2524 match (&expected, &actual) {
2525 (_, TypeExpr::Never) => true,
2527 (TypeExpr::Never, _) => false,
2529 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2530 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2533 act_members.iter().all(|am| {
2534 exp_members
2535 .iter()
2536 .any(|em| self.types_compatible(em, am, scope))
2537 })
2538 }
2539 (TypeExpr::Union(members), actual_type) => members
2540 .iter()
2541 .any(|m| self.types_compatible(m, actual_type, scope)),
2542 (expected_type, TypeExpr::Union(members)) => members
2543 .iter()
2544 .all(|m| self.types_compatible(expected_type, m, scope)),
2545 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2546 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2547 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2548 if expected_field.optional {
2549 return true;
2550 }
2551 af.iter().any(|actual_field| {
2552 actual_field.name == expected_field.name
2553 && self.types_compatible(
2554 &expected_field.type_expr,
2555 &actual_field.type_expr,
2556 scope,
2557 )
2558 })
2559 }),
2560 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2562 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2563 keys_ok
2564 && af
2565 .iter()
2566 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2567 }
2568 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2570 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2571 self.types_compatible(expected_inner, actual_inner, scope)
2572 }
2573 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2574 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2575 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2576 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2577 }
2578 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2579 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2580 (
2582 TypeExpr::FnType {
2583 params: ep,
2584 return_type: er,
2585 },
2586 TypeExpr::FnType {
2587 params: ap,
2588 return_type: ar,
2589 },
2590 ) => {
2591 ep.len() == ap.len()
2592 && ep
2593 .iter()
2594 .zip(ap.iter())
2595 .all(|(e, a)| self.types_compatible(e, a, scope))
2596 && self.types_compatible(er, ar, scope)
2597 }
2598 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2600 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2601 _ => false,
2602 }
2603 }
2604
2605 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2606 if let TypeExpr::Named(name) = ty {
2607 if let Some(resolved) = scope.resolve_type(name) {
2608 return resolved.clone();
2609 }
2610 }
2611 ty.clone()
2612 }
2613
2614 fn error_at(&mut self, message: String, span: Span) {
2615 self.diagnostics.push(TypeDiagnostic {
2616 message,
2617 severity: DiagnosticSeverity::Error,
2618 span: Some(span),
2619 help: None,
2620 fix: None,
2621 });
2622 }
2623
2624 #[allow(dead_code)]
2625 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2626 self.diagnostics.push(TypeDiagnostic {
2627 message,
2628 severity: DiagnosticSeverity::Error,
2629 span: Some(span),
2630 help: Some(help),
2631 fix: None,
2632 });
2633 }
2634
2635 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
2636 self.diagnostics.push(TypeDiagnostic {
2637 message,
2638 severity: DiagnosticSeverity::Error,
2639 span: Some(span),
2640 help: None,
2641 fix: Some(fix),
2642 });
2643 }
2644
2645 fn warning_at(&mut self, message: String, span: Span) {
2646 self.diagnostics.push(TypeDiagnostic {
2647 message,
2648 severity: DiagnosticSeverity::Warning,
2649 span: Some(span),
2650 help: None,
2651 fix: None,
2652 });
2653 }
2654
2655 #[allow(dead_code)]
2656 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2657 self.diagnostics.push(TypeDiagnostic {
2658 message,
2659 severity: DiagnosticSeverity::Warning,
2660 span: Some(span),
2661 help: Some(help),
2662 fix: None,
2663 });
2664 }
2665
2666 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
2670 match &snode.node {
2671 Node::BinaryOp { op, left, right } => {
2672 self.check_binops(left, scope);
2673 self.check_binops(right, scope);
2674 let lt = self.infer_type(left, scope);
2675 let rt = self.infer_type(right, scope);
2676 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
2677 let span = snode.span;
2678 match op.as_str() {
2679 "+" => {
2680 let valid = matches!(
2681 (l.as_str(), r.as_str()),
2682 ("int" | "float", "int" | "float")
2683 | ("string", "string")
2684 | ("list", "list")
2685 | ("dict", "dict")
2686 );
2687 if !valid {
2688 let msg =
2689 format!("Operator '+' is not valid for types {} and {}", l, r);
2690 let fix = if l == "string" || r == "string" {
2691 self.build_interpolation_fix(left, right, l == "string", span)
2692 } else {
2693 None
2694 };
2695 if let Some(fix) = fix {
2696 self.error_at_with_fix(msg, span, fix);
2697 } else {
2698 self.error_at(msg, span);
2699 }
2700 }
2701 }
2702 "-" | "/" | "%" => {
2703 let numeric = ["int", "float"];
2704 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
2705 self.error_at(
2706 format!(
2707 "Operator '{}' requires numeric operands, got {} and {}",
2708 op, l, r
2709 ),
2710 span,
2711 );
2712 }
2713 }
2714 "*" => {
2715 let numeric = ["int", "float"];
2716 let is_numeric =
2717 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
2718 let is_string_repeat =
2719 (l == "string" && r == "int") || (l == "int" && r == "string");
2720 if !is_numeric && !is_string_repeat {
2721 self.error_at(
2722 format!(
2723 "Operator '*' requires numeric operands or string * int, got {} and {}",
2724 l, r
2725 ),
2726 span,
2727 );
2728 }
2729 }
2730 _ => {}
2731 }
2732 }
2733 }
2734 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
2736 _ => {}
2737 }
2738 }
2739
2740 fn build_interpolation_fix(
2742 &self,
2743 left: &SNode,
2744 right: &SNode,
2745 left_is_string: bool,
2746 expr_span: Span,
2747 ) -> Option<Vec<FixEdit>> {
2748 let src = self.source.as_ref()?;
2749 let (str_node, other_node) = if left_is_string {
2750 (left, right)
2751 } else {
2752 (right, left)
2753 };
2754 let str_text = src.get(str_node.span.start..str_node.span.end)?;
2755 let other_text = src.get(other_node.span.start..other_node.span.end)?;
2756 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
2758 if other_text.contains('}') || other_text.contains('"') {
2760 return None;
2761 }
2762 let replacement = if left_is_string {
2763 format!("\"{inner}${{{other_text}}}\"")
2764 } else {
2765 format!("\"${{{other_text}}}{inner}\"")
2766 };
2767 Some(vec![FixEdit {
2768 span: expr_span,
2769 replacement,
2770 }])
2771 }
2772}
2773
2774impl Default for TypeChecker {
2775 fn default() -> Self {
2776 Self::new()
2777 }
2778}
2779
2780fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2782 match op {
2783 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2784 Some(TypeExpr::Named("bool".into()))
2785 }
2786 "+" => match (left, right) {
2787 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2788 match (l.as_str(), r.as_str()) {
2789 ("int", "int") => Some(TypeExpr::Named("int".into())),
2790 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2791 ("string", "string") => Some(TypeExpr::Named("string".into())),
2792 ("list", "list") => Some(TypeExpr::Named("list".into())),
2793 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2794 _ => None,
2795 }
2796 }
2797 _ => None,
2798 },
2799 "-" | "/" | "%" => match (left, right) {
2800 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2801 match (l.as_str(), r.as_str()) {
2802 ("int", "int") => Some(TypeExpr::Named("int".into())),
2803 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2804 _ => None,
2805 }
2806 }
2807 _ => None,
2808 },
2809 "*" => match (left, right) {
2810 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2811 match (l.as_str(), r.as_str()) {
2812 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2813 ("int", "int") => Some(TypeExpr::Named("int".into())),
2814 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2815 _ => None,
2816 }
2817 }
2818 _ => None,
2819 },
2820 "??" => match (left, right) {
2821 (Some(TypeExpr::Union(members)), _) => {
2823 let non_nil: Vec<_> = members
2824 .iter()
2825 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2826 .cloned()
2827 .collect();
2828 if non_nil.len() == 1 {
2829 Some(non_nil[0].clone())
2830 } else if non_nil.is_empty() {
2831 right.clone()
2832 } else {
2833 Some(TypeExpr::Union(non_nil))
2834 }
2835 }
2836 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
2838 (Some(l), _) => Some(l.clone()),
2840 (None, _) => right.clone(),
2842 },
2843 "|>" => None,
2844 _ => None,
2845 }
2846}
2847
2848pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2853 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2854 let mut details = Vec::new();
2855 for field in ef {
2856 if field.optional {
2857 continue;
2858 }
2859 match af.iter().find(|f| f.name == field.name) {
2860 None => details.push(format!(
2861 "missing field '{}' ({})",
2862 field.name,
2863 format_type(&field.type_expr)
2864 )),
2865 Some(actual_field) => {
2866 let e_str = format_type(&field.type_expr);
2867 let a_str = format_type(&actual_field.type_expr);
2868 if e_str != a_str {
2869 details.push(format!(
2870 "field '{}' has type {}, expected {}",
2871 field.name, a_str, e_str
2872 ));
2873 }
2874 }
2875 }
2876 }
2877 if details.is_empty() {
2878 None
2879 } else {
2880 Some(details.join("; "))
2881 }
2882 } else {
2883 None
2884 }
2885}
2886
2887fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
2890 matches!(
2891 &value.node,
2892 Node::IntLiteral(_)
2893 | Node::FloatLiteral(_)
2894 | Node::StringLiteral(_)
2895 | Node::BoolLiteral(_)
2896 | Node::NilLiteral
2897 | Node::ListLiteral(_)
2898 | Node::DictLiteral(_)
2899 | Node::InterpolatedString(_)
2900 )
2901}
2902
2903pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
2906 match &stmt.node {
2907 Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
2908 true
2909 }
2910 Node::IfElse {
2911 then_body,
2912 else_body: Some(else_body),
2913 ..
2914 } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
2915 _ => false,
2916 }
2917}
2918
2919pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
2921 stmts.iter().any(stmt_definitely_exits)
2922}
2923
2924pub fn format_type(ty: &TypeExpr) -> String {
2925 match ty {
2926 TypeExpr::Named(n) => n.clone(),
2927 TypeExpr::Union(types) => types
2928 .iter()
2929 .map(format_type)
2930 .collect::<Vec<_>>()
2931 .join(" | "),
2932 TypeExpr::Shape(fields) => {
2933 let inner: Vec<String> = fields
2934 .iter()
2935 .map(|f| {
2936 let opt = if f.optional { "?" } else { "" };
2937 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2938 })
2939 .collect();
2940 format!("{{{}}}", inner.join(", "))
2941 }
2942 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2943 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2944 TypeExpr::FnType {
2945 params,
2946 return_type,
2947 } => {
2948 let params_str = params
2949 .iter()
2950 .map(format_type)
2951 .collect::<Vec<_>>()
2952 .join(", ");
2953 format!("fn({}) -> {}", params_str, format_type(return_type))
2954 }
2955 TypeExpr::Never => "never".to_string(),
2956 }
2957}
2958
2959fn simplify_union(members: Vec<TypeExpr>) -> TypeExpr {
2961 let filtered: Vec<TypeExpr> = members
2962 .into_iter()
2963 .filter(|m| !matches!(m, TypeExpr::Never))
2964 .collect();
2965 match filtered.len() {
2966 0 => TypeExpr::Never,
2967 1 => filtered.into_iter().next().unwrap(),
2968 _ => TypeExpr::Union(filtered),
2969 }
2970}
2971
2972fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2975 let remaining: Vec<TypeExpr> = members
2976 .iter()
2977 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2978 .cloned()
2979 .collect();
2980 match remaining.len() {
2981 0 => Some(TypeExpr::Never),
2982 1 => Some(remaining.into_iter().next().unwrap()),
2983 _ => Some(TypeExpr::Union(remaining)),
2984 }
2985}
2986
2987fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2989 if members
2990 .iter()
2991 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2992 {
2993 Some(TypeExpr::Named(target.to_string()))
2994 } else {
2995 None
2996 }
2997}
2998
2999fn extract_type_of_var(node: &SNode) -> Option<String> {
3001 if let Node::FunctionCall { name, args } = &node.node {
3002 if name == "type_of" && args.len() == 1 {
3003 if let Node::Identifier(var) = &args[0].node {
3004 return Some(var.clone());
3005 }
3006 }
3007 }
3008 None
3009}
3010
3011fn schema_type_expr_from_node(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3012 match &node.node {
3013 Node::Identifier(name) => scope.get_schema_binding(name).cloned().flatten(),
3014 Node::DictLiteral(entries) => schema_type_expr_from_dict(entries, scope),
3015 _ => None,
3016 }
3017}
3018
3019fn schema_type_expr_from_dict(entries: &[DictEntry], scope: &TypeScope) -> Option<TypeExpr> {
3020 let mut type_name: Option<String> = None;
3021 let mut properties: Option<&SNode> = None;
3022 let mut required: Option<Vec<String>> = None;
3023 let mut items: Option<&SNode> = None;
3024 let mut union: Option<&SNode> = None;
3025 let mut nullable = false;
3026 let mut additional_properties: Option<&SNode> = None;
3027
3028 for entry in entries {
3029 let key = schema_entry_key(&entry.key)?;
3030 match key.as_str() {
3031 "type" => match &entry.value.node {
3032 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3033 type_name = Some(normalize_schema_type_name(text));
3034 }
3035 Node::ListLiteral(items_list) => {
3036 let union_members = items_list
3037 .iter()
3038 .filter_map(|item| match &item.node {
3039 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3040 Some(TypeExpr::Named(normalize_schema_type_name(text)))
3041 }
3042 _ => None,
3043 })
3044 .collect::<Vec<_>>();
3045 if !union_members.is_empty() {
3046 return Some(TypeExpr::Union(union_members));
3047 }
3048 }
3049 _ => {}
3050 },
3051 "properties" => properties = Some(&entry.value),
3052 "required" => {
3053 required = schema_required_names(&entry.value);
3054 }
3055 "items" => items = Some(&entry.value),
3056 "union" | "oneOf" | "anyOf" => union = Some(&entry.value),
3057 "nullable" => {
3058 nullable = matches!(entry.value.node, Node::BoolLiteral(true));
3059 }
3060 "additional_properties" | "additionalProperties" => {
3061 additional_properties = Some(&entry.value);
3062 }
3063 _ => {}
3064 }
3065 }
3066
3067 let mut schema_type = if let Some(union_node) = union {
3068 schema_union_type_expr(union_node, scope)?
3069 } else if let Some(properties_node) = properties {
3070 let property_entries = match &properties_node.node {
3071 Node::DictLiteral(entries) => entries,
3072 _ => return None,
3073 };
3074 let required_names = required.unwrap_or_default();
3075 let mut fields = Vec::new();
3076 for entry in property_entries {
3077 let field_name = schema_entry_key(&entry.key)?;
3078 let field_type = schema_type_expr_from_node(&entry.value, scope)?;
3079 fields.push(ShapeField {
3080 name: field_name.clone(),
3081 type_expr: field_type,
3082 optional: !required_names.contains(&field_name),
3083 });
3084 }
3085 TypeExpr::Shape(fields)
3086 } else if let Some(item_node) = items {
3087 TypeExpr::List(Box::new(schema_type_expr_from_node(item_node, scope)?))
3088 } else if let Some(type_name) = type_name {
3089 if type_name == "dict" {
3090 if let Some(extra_node) = additional_properties {
3091 let value_type = match &extra_node.node {
3092 Node::BoolLiteral(_) => None,
3093 _ => schema_type_expr_from_node(extra_node, scope),
3094 };
3095 if let Some(value_type) = value_type {
3096 TypeExpr::DictType(
3097 Box::new(TypeExpr::Named("string".into())),
3098 Box::new(value_type),
3099 )
3100 } else {
3101 TypeExpr::Named(type_name)
3102 }
3103 } else {
3104 TypeExpr::Named(type_name)
3105 }
3106 } else {
3107 TypeExpr::Named(type_name)
3108 }
3109 } else {
3110 return None;
3111 };
3112
3113 if nullable {
3114 schema_type = match schema_type {
3115 TypeExpr::Union(mut members) => {
3116 if !members
3117 .iter()
3118 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil"))
3119 {
3120 members.push(TypeExpr::Named("nil".into()));
3121 }
3122 TypeExpr::Union(members)
3123 }
3124 other => TypeExpr::Union(vec![other, TypeExpr::Named("nil".into())]),
3125 };
3126 }
3127
3128 Some(schema_type)
3129}
3130
3131fn schema_union_type_expr(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3132 let Node::ListLiteral(items) = &node.node else {
3133 return None;
3134 };
3135 let members = items
3136 .iter()
3137 .filter_map(|item| schema_type_expr_from_node(item, scope))
3138 .collect::<Vec<_>>();
3139 match members.len() {
3140 0 => None,
3141 1 => members.into_iter().next(),
3142 _ => Some(TypeExpr::Union(members)),
3143 }
3144}
3145
3146fn schema_required_names(node: &SNode) -> Option<Vec<String>> {
3147 let Node::ListLiteral(items) = &node.node else {
3148 return None;
3149 };
3150 Some(
3151 items
3152 .iter()
3153 .filter_map(|item| match &item.node {
3154 Node::StringLiteral(text) | Node::RawStringLiteral(text) => Some(text.clone()),
3155 Node::Identifier(text) => Some(text.clone()),
3156 _ => None,
3157 })
3158 .collect(),
3159 )
3160}
3161
3162fn schema_entry_key(node: &SNode) -> Option<String> {
3163 match &node.node {
3164 Node::Identifier(name) => Some(name.clone()),
3165 Node::StringLiteral(name) | Node::RawStringLiteral(name) => Some(name.clone()),
3166 _ => None,
3167 }
3168}
3169
3170fn normalize_schema_type_name(text: &str) -> String {
3171 match text {
3172 "object" => "dict".into(),
3173 "array" => "list".into(),
3174 "integer" => "int".into(),
3175 "number" => "float".into(),
3176 "boolean" => "bool".into(),
3177 "null" => "nil".into(),
3178 other => other.into(),
3179 }
3180}
3181
3182fn intersect_types(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
3183 match (current, schema_type) {
3184 (TypeExpr::Union(members), other) => {
3185 let kept = members
3186 .iter()
3187 .filter_map(|member| intersect_types(member, other))
3188 .collect::<Vec<_>>();
3189 match kept.len() {
3190 0 => None,
3191 1 => kept.into_iter().next(),
3192 _ => Some(TypeExpr::Union(kept)),
3193 }
3194 }
3195 (other, TypeExpr::Union(members)) => {
3196 let kept = members
3197 .iter()
3198 .filter_map(|member| intersect_types(other, member))
3199 .collect::<Vec<_>>();
3200 match kept.len() {
3201 0 => None,
3202 1 => kept.into_iter().next(),
3203 _ => Some(TypeExpr::Union(kept)),
3204 }
3205 }
3206 (TypeExpr::Named(left), TypeExpr::Named(right)) if left == right => {
3207 Some(TypeExpr::Named(left.clone()))
3208 }
3209 (TypeExpr::Named(name), TypeExpr::Shape(fields)) if name == "dict" => {
3210 Some(TypeExpr::Shape(fields.clone()))
3211 }
3212 (TypeExpr::Shape(fields), TypeExpr::Named(name)) if name == "dict" => {
3213 Some(TypeExpr::Shape(fields.clone()))
3214 }
3215 (TypeExpr::Named(name), TypeExpr::List(inner)) if name == "list" => {
3216 Some(TypeExpr::List(inner.clone()))
3217 }
3218 (TypeExpr::List(inner), TypeExpr::Named(name)) if name == "list" => {
3219 Some(TypeExpr::List(inner.clone()))
3220 }
3221 (TypeExpr::Named(name), TypeExpr::DictType(key, value)) if name == "dict" => {
3222 Some(TypeExpr::DictType(key.clone(), value.clone()))
3223 }
3224 (TypeExpr::DictType(key, value), TypeExpr::Named(name)) if name == "dict" => {
3225 Some(TypeExpr::DictType(key.clone(), value.clone()))
3226 }
3227 (TypeExpr::Shape(_), TypeExpr::Shape(fields)) => Some(TypeExpr::Shape(fields.clone())),
3228 (TypeExpr::List(current_inner), TypeExpr::List(schema_inner)) => {
3229 intersect_types(current_inner, schema_inner)
3230 .map(|inner| TypeExpr::List(Box::new(inner)))
3231 }
3232 (
3233 TypeExpr::DictType(current_key, current_value),
3234 TypeExpr::DictType(schema_key, schema_value),
3235 ) => {
3236 let key = intersect_types(current_key, schema_key)?;
3237 let value = intersect_types(current_value, schema_value)?;
3238 Some(TypeExpr::DictType(Box::new(key), Box::new(value)))
3239 }
3240 _ => None,
3241 }
3242}
3243
3244fn subtract_type(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
3245 match current {
3246 TypeExpr::Union(members) => {
3247 let remaining = members
3248 .iter()
3249 .filter(|member| intersect_types(member, schema_type).is_none())
3250 .cloned()
3251 .collect::<Vec<_>>();
3252 match remaining.len() {
3253 0 => None,
3254 1 => remaining.into_iter().next(),
3255 _ => Some(TypeExpr::Union(remaining)),
3256 }
3257 }
3258 other if intersect_types(other, schema_type).is_some() => None,
3259 other => Some(other.clone()),
3260 }
3261}
3262
3263fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
3265 for (var_name, narrowed_type) in refinements {
3266 if !scope.narrowed_vars.contains_key(var_name) {
3268 if let Some(original) = scope.get_var(var_name).cloned() {
3269 scope.narrowed_vars.insert(var_name.clone(), original);
3270 }
3271 }
3272 scope.define_var(var_name, narrowed_type.clone());
3273 }
3274}
3275
3276#[cfg(test)]
3277mod tests {
3278 use super::*;
3279 use crate::Parser;
3280 use harn_lexer::Lexer;
3281
3282 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
3283 let mut lexer = Lexer::new(source);
3284 let tokens = lexer.tokenize().unwrap();
3285 let mut parser = Parser::new(tokens);
3286 let program = parser.parse().unwrap();
3287 TypeChecker::new().check(&program)
3288 }
3289
3290 fn errors(source: &str) -> Vec<String> {
3291 check_source(source)
3292 .into_iter()
3293 .filter(|d| d.severity == DiagnosticSeverity::Error)
3294 .map(|d| d.message)
3295 .collect()
3296 }
3297
3298 #[test]
3299 fn test_no_errors_for_untyped_code() {
3300 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
3301 assert!(errs.is_empty());
3302 }
3303
3304 #[test]
3305 fn test_correct_typed_let() {
3306 let errs = errors("pipeline t(task) { let x: int = 42 }");
3307 assert!(errs.is_empty());
3308 }
3309
3310 #[test]
3311 fn test_type_mismatch_let() {
3312 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
3313 assert_eq!(errs.len(), 1);
3314 assert!(errs[0].contains("Type mismatch"));
3315 assert!(errs[0].contains("int"));
3316 assert!(errs[0].contains("string"));
3317 }
3318
3319 #[test]
3320 fn test_correct_typed_fn() {
3321 let errs = errors(
3322 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
3323 );
3324 assert!(errs.is_empty());
3325 }
3326
3327 #[test]
3328 fn test_fn_arg_type_mismatch() {
3329 let errs = errors(
3330 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
3331add("hello", 2) }"#,
3332 );
3333 assert_eq!(errs.len(), 1);
3334 assert!(errs[0].contains("Argument 1"));
3335 assert!(errs[0].contains("expected int"));
3336 }
3337
3338 #[test]
3339 fn test_return_type_mismatch() {
3340 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
3341 assert_eq!(errs.len(), 1);
3342 assert!(errs[0].contains("Return type mismatch"));
3343 }
3344
3345 #[test]
3346 fn test_union_type_compatible() {
3347 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
3348 assert!(errs.is_empty());
3349 }
3350
3351 #[test]
3352 fn test_union_type_mismatch() {
3353 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
3354 assert_eq!(errs.len(), 1);
3355 assert!(errs[0].contains("Type mismatch"));
3356 }
3357
3358 #[test]
3359 fn test_type_inference_propagation() {
3360 let errs = errors(
3361 r#"pipeline t(task) {
3362 fn add(a: int, b: int) -> int { return a + b }
3363 let result: string = add(1, 2)
3364}"#,
3365 );
3366 assert_eq!(errs.len(), 1);
3367 assert!(errs[0].contains("Type mismatch"));
3368 assert!(errs[0].contains("string"));
3369 assert!(errs[0].contains("int"));
3370 }
3371
3372 #[test]
3373 fn test_generic_return_type_instantiates_from_callsite() {
3374 let errs = errors(
3375 r#"pipeline t(task) {
3376 fn identity<T>(x: T) -> T { return x }
3377 fn first<T>(items: list<T>) -> T { return items[0] }
3378 let n: int = identity(42)
3379 let s: string = first(["a", "b"])
3380}"#,
3381 );
3382 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
3383 }
3384
3385 #[test]
3386 fn test_generic_type_param_must_bind_consistently() {
3387 let errs = errors(
3388 r#"pipeline t(task) {
3389 fn keep<T>(a: T, b: T) -> T { return a }
3390 keep(1, "x")
3391}"#,
3392 );
3393 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
3394 assert!(
3395 errs.iter()
3396 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
3397 "missing generic binding conflict error: {:?}",
3398 errs
3399 );
3400 assert!(
3401 errs.iter()
3402 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
3403 "missing instantiated argument mismatch error: {:?}",
3404 errs
3405 );
3406 }
3407
3408 #[test]
3409 fn test_generic_list_binding_propagates_element_type() {
3410 let errs = errors(
3411 r#"pipeline t(task) {
3412 fn first<T>(items: list<T>) -> T { return items[0] }
3413 let bad: string = first([1, 2, 3])
3414}"#,
3415 );
3416 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3417 assert!(errs[0].contains("declared as string, but assigned int"));
3418 }
3419
3420 #[test]
3421 fn test_builtin_return_type_inference() {
3422 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
3423 assert_eq!(errs.len(), 1);
3424 assert!(errs[0].contains("string"));
3425 assert!(errs[0].contains("int"));
3426 }
3427
3428 #[test]
3429 fn test_workflow_and_transcript_builtins_are_known() {
3430 let errs = errors(
3431 r#"pipeline t(task) {
3432 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
3433 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
3434 let run: dict = workflow_execute("task", flow, [], {})
3435 let tree: dict = load_run_tree("run.json")
3436 let fixture: dict = run_record_fixture(run?.run)
3437 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
3438 let diff: dict = run_record_diff(run?.run, run?.run)
3439 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
3440 let suite_report: dict = eval_suite_run(manifest)
3441 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
3442 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
3443 let selection: dict = artifact_editor_selection("src/main.rs", "main")
3444 let verify: dict = artifact_verification_result("verify", "ok")
3445 let test_result: dict = artifact_test_result("tests", "pass")
3446 let cmd: dict = artifact_command_result("cargo test", {status: 0})
3447 let patch: dict = artifact_diff("src/main.rs", "old", "new")
3448 let git: dict = artifact_git_diff("diff --git a b")
3449 let review: dict = artifact_diff_review(patch, "review me")
3450 let decision: dict = artifact_review_decision(review, "accepted")
3451 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
3452 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
3453 let apply: dict = artifact_apply_intent(review, "apply")
3454 let transcript = transcript_reset({metadata: {source: "test"}})
3455 let visible: string = transcript_render_visible(transcript_archive(transcript))
3456 let events: list = transcript_events(transcript)
3457 let context: string = artifact_context([], {max_artifacts: 1})
3458 println(report)
3459 println(run)
3460 println(tree)
3461 println(fixture)
3462 println(suite)
3463 println(diff)
3464 println(manifest)
3465 println(suite_report)
3466 println(wf)
3467 println(snap)
3468 println(selection)
3469 println(verify)
3470 println(test_result)
3471 println(cmd)
3472 println(patch)
3473 println(git)
3474 println(review)
3475 println(decision)
3476 println(proposal)
3477 println(bundle)
3478 println(apply)
3479 println(visible)
3480 println(events)
3481 println(context)
3482}"#,
3483 );
3484 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
3485 }
3486
3487 #[test]
3488 fn test_binary_op_type_inference() {
3489 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
3490 assert_eq!(errs.len(), 1);
3491 }
3492
3493 #[test]
3494 fn test_comparison_returns_bool() {
3495 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
3496 assert!(errs.is_empty());
3497 }
3498
3499 #[test]
3500 fn test_int_float_promotion() {
3501 let errs = errors("pipeline t(task) { let x: float = 42 }");
3502 assert!(errs.is_empty());
3503 }
3504
3505 #[test]
3506 fn test_untyped_code_no_errors() {
3507 let errs = errors(
3508 r#"pipeline t(task) {
3509 fn process(data) {
3510 let result = data + " processed"
3511 return result
3512 }
3513 log(process("hello"))
3514}"#,
3515 );
3516 assert!(errs.is_empty());
3517 }
3518
3519 #[test]
3520 fn test_type_alias() {
3521 let errs = errors(
3522 r#"pipeline t(task) {
3523 type Name = string
3524 let x: Name = "hello"
3525}"#,
3526 );
3527 assert!(errs.is_empty());
3528 }
3529
3530 #[test]
3531 fn test_type_alias_mismatch() {
3532 let errs = errors(
3533 r#"pipeline t(task) {
3534 type Name = string
3535 let x: Name = 42
3536}"#,
3537 );
3538 assert_eq!(errs.len(), 1);
3539 }
3540
3541 #[test]
3542 fn test_assignment_type_check() {
3543 let errs = errors(
3544 r#"pipeline t(task) {
3545 var x: int = 0
3546 x = "hello"
3547}"#,
3548 );
3549 assert_eq!(errs.len(), 1);
3550 assert!(errs[0].contains("cannot assign string"));
3551 }
3552
3553 #[test]
3554 fn test_covariance_int_to_float_in_fn() {
3555 let errs = errors(
3556 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
3557 );
3558 assert!(errs.is_empty());
3559 }
3560
3561 #[test]
3562 fn test_covariance_return_type() {
3563 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
3564 assert!(errs.is_empty());
3565 }
3566
3567 #[test]
3568 fn test_no_contravariance_float_to_int() {
3569 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
3570 assert_eq!(errs.len(), 1);
3571 }
3572
3573 fn warnings(source: &str) -> Vec<String> {
3576 check_source(source)
3577 .into_iter()
3578 .filter(|d| d.severity == DiagnosticSeverity::Warning)
3579 .map(|d| d.message)
3580 .collect()
3581 }
3582
3583 #[test]
3584 fn test_exhaustive_match_no_warning() {
3585 let warns = warnings(
3586 r#"pipeline t(task) {
3587 enum Color { Red, Green, Blue }
3588 let c = Color.Red
3589 match c.variant {
3590 "Red" -> { log("r") }
3591 "Green" -> { log("g") }
3592 "Blue" -> { log("b") }
3593 }
3594}"#,
3595 );
3596 let exhaustive_warns: Vec<_> = warns
3597 .iter()
3598 .filter(|w| w.contains("Non-exhaustive"))
3599 .collect();
3600 assert!(exhaustive_warns.is_empty());
3601 }
3602
3603 #[test]
3604 fn test_non_exhaustive_match_warning() {
3605 let warns = warnings(
3606 r#"pipeline t(task) {
3607 enum Color { Red, Green, Blue }
3608 let c = Color.Red
3609 match c.variant {
3610 "Red" -> { log("r") }
3611 "Green" -> { log("g") }
3612 }
3613}"#,
3614 );
3615 let exhaustive_warns: Vec<_> = warns
3616 .iter()
3617 .filter(|w| w.contains("Non-exhaustive"))
3618 .collect();
3619 assert_eq!(exhaustive_warns.len(), 1);
3620 assert!(exhaustive_warns[0].contains("Blue"));
3621 }
3622
3623 #[test]
3624 fn test_non_exhaustive_multiple_missing() {
3625 let warns = warnings(
3626 r#"pipeline t(task) {
3627 enum Status { Active, Inactive, Pending }
3628 let s = Status.Active
3629 match s.variant {
3630 "Active" -> { log("a") }
3631 }
3632}"#,
3633 );
3634 let exhaustive_warns: Vec<_> = warns
3635 .iter()
3636 .filter(|w| w.contains("Non-exhaustive"))
3637 .collect();
3638 assert_eq!(exhaustive_warns.len(), 1);
3639 assert!(exhaustive_warns[0].contains("Inactive"));
3640 assert!(exhaustive_warns[0].contains("Pending"));
3641 }
3642
3643 #[test]
3644 fn test_enum_construct_type_inference() {
3645 let errs = errors(
3646 r#"pipeline t(task) {
3647 enum Color { Red, Green, Blue }
3648 let c: Color = Color.Red
3649}"#,
3650 );
3651 assert!(errs.is_empty());
3652 }
3653
3654 #[test]
3657 fn test_nil_coalescing_strips_nil() {
3658 let errs = errors(
3660 r#"pipeline t(task) {
3661 let x: string | nil = nil
3662 let y: string = x ?? "default"
3663}"#,
3664 );
3665 assert!(errs.is_empty());
3666 }
3667
3668 #[test]
3669 fn test_shape_mismatch_detail_missing_field() {
3670 let errs = errors(
3671 r#"pipeline t(task) {
3672 let x: {name: string, age: int} = {name: "hello"}
3673}"#,
3674 );
3675 assert_eq!(errs.len(), 1);
3676 assert!(
3677 errs[0].contains("missing field 'age'"),
3678 "expected detail about missing field, got: {}",
3679 errs[0]
3680 );
3681 }
3682
3683 #[test]
3684 fn test_shape_mismatch_detail_wrong_type() {
3685 let errs = errors(
3686 r#"pipeline t(task) {
3687 let x: {name: string, age: int} = {name: 42, age: 10}
3688}"#,
3689 );
3690 assert_eq!(errs.len(), 1);
3691 assert!(
3692 errs[0].contains("field 'name' has type int, expected string"),
3693 "expected detail about wrong type, got: {}",
3694 errs[0]
3695 );
3696 }
3697
3698 #[test]
3701 fn test_match_pattern_string_against_int() {
3702 let warns = warnings(
3703 r#"pipeline t(task) {
3704 let x: int = 42
3705 match x {
3706 "hello" -> { log("bad") }
3707 42 -> { log("ok") }
3708 }
3709}"#,
3710 );
3711 let pattern_warns: Vec<_> = warns
3712 .iter()
3713 .filter(|w| w.contains("Match pattern type mismatch"))
3714 .collect();
3715 assert_eq!(pattern_warns.len(), 1);
3716 assert!(pattern_warns[0].contains("matching int against string literal"));
3717 }
3718
3719 #[test]
3720 fn test_match_pattern_int_against_string() {
3721 let warns = warnings(
3722 r#"pipeline t(task) {
3723 let x: string = "hello"
3724 match x {
3725 42 -> { log("bad") }
3726 "hello" -> { log("ok") }
3727 }
3728}"#,
3729 );
3730 let pattern_warns: Vec<_> = warns
3731 .iter()
3732 .filter(|w| w.contains("Match pattern type mismatch"))
3733 .collect();
3734 assert_eq!(pattern_warns.len(), 1);
3735 assert!(pattern_warns[0].contains("matching string against int literal"));
3736 }
3737
3738 #[test]
3739 fn test_match_pattern_bool_against_int() {
3740 let warns = warnings(
3741 r#"pipeline t(task) {
3742 let x: int = 42
3743 match x {
3744 true -> { log("bad") }
3745 42 -> { log("ok") }
3746 }
3747}"#,
3748 );
3749 let pattern_warns: Vec<_> = warns
3750 .iter()
3751 .filter(|w| w.contains("Match pattern type mismatch"))
3752 .collect();
3753 assert_eq!(pattern_warns.len(), 1);
3754 assert!(pattern_warns[0].contains("matching int against bool literal"));
3755 }
3756
3757 #[test]
3758 fn test_match_pattern_float_against_string() {
3759 let warns = warnings(
3760 r#"pipeline t(task) {
3761 let x: string = "hello"
3762 match x {
3763 3.14 -> { log("bad") }
3764 "hello" -> { log("ok") }
3765 }
3766}"#,
3767 );
3768 let pattern_warns: Vec<_> = warns
3769 .iter()
3770 .filter(|w| w.contains("Match pattern type mismatch"))
3771 .collect();
3772 assert_eq!(pattern_warns.len(), 1);
3773 assert!(pattern_warns[0].contains("matching string against float literal"));
3774 }
3775
3776 #[test]
3777 fn test_match_pattern_int_against_float_ok() {
3778 let warns = warnings(
3780 r#"pipeline t(task) {
3781 let x: float = 3.14
3782 match x {
3783 42 -> { log("ok") }
3784 _ -> { log("default") }
3785 }
3786}"#,
3787 );
3788 let pattern_warns: Vec<_> = warns
3789 .iter()
3790 .filter(|w| w.contains("Match pattern type mismatch"))
3791 .collect();
3792 assert!(pattern_warns.is_empty());
3793 }
3794
3795 #[test]
3796 fn test_match_pattern_float_against_int_ok() {
3797 let warns = warnings(
3799 r#"pipeline t(task) {
3800 let x: int = 42
3801 match x {
3802 3.14 -> { log("close") }
3803 _ -> { log("default") }
3804 }
3805}"#,
3806 );
3807 let pattern_warns: Vec<_> = warns
3808 .iter()
3809 .filter(|w| w.contains("Match pattern type mismatch"))
3810 .collect();
3811 assert!(pattern_warns.is_empty());
3812 }
3813
3814 #[test]
3815 fn test_match_pattern_correct_types_no_warning() {
3816 let warns = warnings(
3817 r#"pipeline t(task) {
3818 let x: int = 42
3819 match x {
3820 1 -> { log("one") }
3821 2 -> { log("two") }
3822 _ -> { log("other") }
3823 }
3824}"#,
3825 );
3826 let pattern_warns: Vec<_> = warns
3827 .iter()
3828 .filter(|w| w.contains("Match pattern type mismatch"))
3829 .collect();
3830 assert!(pattern_warns.is_empty());
3831 }
3832
3833 #[test]
3834 fn test_match_pattern_wildcard_no_warning() {
3835 let warns = warnings(
3836 r#"pipeline t(task) {
3837 let x: int = 42
3838 match x {
3839 _ -> { log("catch all") }
3840 }
3841}"#,
3842 );
3843 let pattern_warns: Vec<_> = warns
3844 .iter()
3845 .filter(|w| w.contains("Match pattern type mismatch"))
3846 .collect();
3847 assert!(pattern_warns.is_empty());
3848 }
3849
3850 #[test]
3851 fn test_match_pattern_untyped_no_warning() {
3852 let warns = warnings(
3854 r#"pipeline t(task) {
3855 let x = some_unknown_fn()
3856 match x {
3857 "hello" -> { log("string") }
3858 42 -> { log("int") }
3859 }
3860}"#,
3861 );
3862 let pattern_warns: Vec<_> = warns
3863 .iter()
3864 .filter(|w| w.contains("Match pattern type mismatch"))
3865 .collect();
3866 assert!(pattern_warns.is_empty());
3867 }
3868
3869 fn iface_errors(source: &str) -> Vec<String> {
3872 errors(source)
3873 .into_iter()
3874 .filter(|message| message.contains("does not satisfy interface"))
3875 .collect()
3876 }
3877
3878 #[test]
3879 fn test_interface_constraint_return_type_mismatch() {
3880 let warns = iface_errors(
3881 r#"pipeline t(task) {
3882 interface Sizable {
3883 fn size(self) -> int
3884 }
3885 struct Box { width: int }
3886 impl Box {
3887 fn size(self) -> string { return "nope" }
3888 }
3889 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3890 measure(Box({width: 3}))
3891}"#,
3892 );
3893 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3894 assert!(
3895 warns[0].contains("method 'size' returns 'string', expected 'int'"),
3896 "unexpected message: {}",
3897 warns[0]
3898 );
3899 }
3900
3901 #[test]
3902 fn test_interface_constraint_param_type_mismatch() {
3903 let warns = iface_errors(
3904 r#"pipeline t(task) {
3905 interface Processor {
3906 fn process(self, x: int) -> string
3907 }
3908 struct MyProc { name: string }
3909 impl MyProc {
3910 fn process(self, x: string) -> string { return x }
3911 }
3912 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
3913 run_proc(MyProc({name: "a"}))
3914}"#,
3915 );
3916 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3917 assert!(
3918 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
3919 "unexpected message: {}",
3920 warns[0]
3921 );
3922 }
3923
3924 #[test]
3925 fn test_interface_constraint_missing_method() {
3926 let warns = iface_errors(
3927 r#"pipeline t(task) {
3928 interface Sizable {
3929 fn size(self) -> int
3930 }
3931 struct Box { width: int }
3932 impl Box {
3933 fn area(self) -> int { return self.width }
3934 }
3935 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3936 measure(Box({width: 3}))
3937}"#,
3938 );
3939 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3940 assert!(
3941 warns[0].contains("missing method 'size'"),
3942 "unexpected message: {}",
3943 warns[0]
3944 );
3945 }
3946
3947 #[test]
3948 fn test_interface_constraint_param_count_mismatch() {
3949 let warns = iface_errors(
3950 r#"pipeline t(task) {
3951 interface Doubler {
3952 fn double(self, x: int) -> int
3953 }
3954 struct Bad { v: int }
3955 impl Bad {
3956 fn double(self) -> int { return self.v * 2 }
3957 }
3958 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3959 run_double(Bad({v: 5}))
3960}"#,
3961 );
3962 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3963 assert!(
3964 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3965 "unexpected message: {}",
3966 warns[0]
3967 );
3968 }
3969
3970 #[test]
3971 fn test_interface_constraint_satisfied() {
3972 let warns = iface_errors(
3973 r#"pipeline t(task) {
3974 interface Sizable {
3975 fn size(self) -> int
3976 }
3977 struct Box { width: int, height: int }
3978 impl Box {
3979 fn size(self) -> int { return self.width * self.height }
3980 }
3981 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3982 measure(Box({width: 3, height: 4}))
3983}"#,
3984 );
3985 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3986 }
3987
3988 #[test]
3989 fn test_interface_constraint_untyped_impl_compatible() {
3990 let warns = iface_errors(
3992 r#"pipeline t(task) {
3993 interface Sizable {
3994 fn size(self) -> int
3995 }
3996 struct Box { width: int }
3997 impl Box {
3998 fn size(self) { return self.width }
3999 }
4000 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4001 measure(Box({width: 3}))
4002}"#,
4003 );
4004 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4005 }
4006
4007 #[test]
4008 fn test_interface_constraint_int_float_covariance() {
4009 let warns = iface_errors(
4011 r#"pipeline t(task) {
4012 interface Measurable {
4013 fn value(self) -> float
4014 }
4015 struct Gauge { v: int }
4016 impl Gauge {
4017 fn value(self) -> int { return self.v }
4018 }
4019 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
4020 read_val(Gauge({v: 42}))
4021}"#,
4022 );
4023 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4024 }
4025
4026 #[test]
4029 fn test_nil_narrowing_then_branch() {
4030 let errs = errors(
4032 r#"pipeline t(task) {
4033 fn greet(name: string | nil) {
4034 if name != nil {
4035 let s: string = name
4036 }
4037 }
4038}"#,
4039 );
4040 assert!(errs.is_empty(), "got: {:?}", errs);
4041 }
4042
4043 #[test]
4044 fn test_nil_narrowing_else_branch() {
4045 let errs = errors(
4047 r#"pipeline t(task) {
4048 fn check(x: string | nil) {
4049 if x != nil {
4050 let s: string = x
4051 } else {
4052 let n: nil = x
4053 }
4054 }
4055}"#,
4056 );
4057 assert!(errs.is_empty(), "got: {:?}", errs);
4058 }
4059
4060 #[test]
4061 fn test_nil_equality_narrows_both() {
4062 let errs = errors(
4064 r#"pipeline t(task) {
4065 fn check(x: string | nil) {
4066 if x == nil {
4067 let n: nil = x
4068 } else {
4069 let s: string = x
4070 }
4071 }
4072}"#,
4073 );
4074 assert!(errs.is_empty(), "got: {:?}", errs);
4075 }
4076
4077 #[test]
4078 fn test_truthiness_narrowing() {
4079 let errs = errors(
4081 r#"pipeline t(task) {
4082 fn check(x: string | nil) {
4083 if x {
4084 let s: string = x
4085 }
4086 }
4087}"#,
4088 );
4089 assert!(errs.is_empty(), "got: {:?}", errs);
4090 }
4091
4092 #[test]
4093 fn test_negation_narrowing() {
4094 let errs = errors(
4096 r#"pipeline t(task) {
4097 fn check(x: string | nil) {
4098 if !x {
4099 let n: nil = x
4100 } else {
4101 let s: string = x
4102 }
4103 }
4104}"#,
4105 );
4106 assert!(errs.is_empty(), "got: {:?}", errs);
4107 }
4108
4109 #[test]
4110 fn test_typeof_narrowing() {
4111 let errs = errors(
4113 r#"pipeline t(task) {
4114 fn check(x: string | int) {
4115 if type_of(x) == "string" {
4116 let s: string = x
4117 }
4118 }
4119}"#,
4120 );
4121 assert!(errs.is_empty(), "got: {:?}", errs);
4122 }
4123
4124 #[test]
4125 fn test_typeof_narrowing_else() {
4126 let errs = errors(
4128 r#"pipeline t(task) {
4129 fn check(x: string | int) {
4130 if type_of(x) == "string" {
4131 let s: string = x
4132 } else {
4133 let i: int = x
4134 }
4135 }
4136}"#,
4137 );
4138 assert!(errs.is_empty(), "got: {:?}", errs);
4139 }
4140
4141 #[test]
4142 fn test_typeof_neq_narrowing() {
4143 let errs = errors(
4145 r#"pipeline t(task) {
4146 fn check(x: string | int) {
4147 if type_of(x) != "string" {
4148 let i: int = x
4149 } else {
4150 let s: string = x
4151 }
4152 }
4153}"#,
4154 );
4155 assert!(errs.is_empty(), "got: {:?}", errs);
4156 }
4157
4158 #[test]
4159 fn test_and_combines_narrowing() {
4160 let errs = errors(
4162 r#"pipeline t(task) {
4163 fn check(x: string | int | nil) {
4164 if x != nil && type_of(x) == "string" {
4165 let s: string = x
4166 }
4167 }
4168}"#,
4169 );
4170 assert!(errs.is_empty(), "got: {:?}", errs);
4171 }
4172
4173 #[test]
4174 fn test_or_falsy_narrowing() {
4175 let errs = errors(
4177 r#"pipeline t(task) {
4178 fn check(x: string | nil, y: int | nil) {
4179 if x || y {
4180 // conservative: can't narrow
4181 } else {
4182 let xn: nil = x
4183 let yn: nil = y
4184 }
4185 }
4186}"#,
4187 );
4188 assert!(errs.is_empty(), "got: {:?}", errs);
4189 }
4190
4191 #[test]
4192 fn test_guard_narrows_outer_scope() {
4193 let errs = errors(
4194 r#"pipeline t(task) {
4195 fn check(x: string | nil) {
4196 guard x != nil else { return }
4197 let s: string = x
4198 }
4199}"#,
4200 );
4201 assert!(errs.is_empty(), "got: {:?}", errs);
4202 }
4203
4204 #[test]
4205 fn test_while_narrows_body() {
4206 let errs = errors(
4207 r#"pipeline t(task) {
4208 fn check(x: string | nil) {
4209 while x != nil {
4210 let s: string = x
4211 break
4212 }
4213 }
4214}"#,
4215 );
4216 assert!(errs.is_empty(), "got: {:?}", errs);
4217 }
4218
4219 #[test]
4220 fn test_early_return_narrows_after_if() {
4221 let errs = errors(
4223 r#"pipeline t(task) {
4224 fn check(x: string | nil) -> string {
4225 if x == nil {
4226 return "default"
4227 }
4228 let s: string = x
4229 return s
4230 }
4231}"#,
4232 );
4233 assert!(errs.is_empty(), "got: {:?}", errs);
4234 }
4235
4236 #[test]
4237 fn test_early_throw_narrows_after_if() {
4238 let errs = errors(
4239 r#"pipeline t(task) {
4240 fn check(x: string | nil) {
4241 if x == nil {
4242 throw "missing"
4243 }
4244 let s: string = x
4245 }
4246}"#,
4247 );
4248 assert!(errs.is_empty(), "got: {:?}", errs);
4249 }
4250
4251 #[test]
4252 fn test_no_narrowing_unknown_type() {
4253 let errs = errors(
4255 r#"pipeline t(task) {
4256 fn check(x) {
4257 if x != nil {
4258 let s: string = x
4259 }
4260 }
4261}"#,
4262 );
4263 assert!(errs.is_empty(), "got: {:?}", errs);
4266 }
4267
4268 #[test]
4269 fn test_reassignment_invalidates_narrowing() {
4270 let errs = errors(
4272 r#"pipeline t(task) {
4273 fn check(x: string | nil) {
4274 var y: string | nil = x
4275 if y != nil {
4276 let s: string = y
4277 y = nil
4278 let s2: string = y
4279 }
4280 }
4281}"#,
4282 );
4283 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
4285 assert!(
4286 errs[0].contains("Type mismatch"),
4287 "expected type mismatch, got: {}",
4288 errs[0]
4289 );
4290 }
4291
4292 #[test]
4293 fn test_let_immutable_warning() {
4294 let all = check_source(
4295 r#"pipeline t(task) {
4296 let x = 42
4297 x = 43
4298}"#,
4299 );
4300 let warnings: Vec<_> = all
4301 .iter()
4302 .filter(|d| d.severity == DiagnosticSeverity::Warning)
4303 .collect();
4304 assert!(
4305 warnings.iter().any(|w| w.message.contains("immutable")),
4306 "expected immutability warning, got: {:?}",
4307 warnings
4308 );
4309 }
4310
4311 #[test]
4312 fn test_nested_narrowing() {
4313 let errs = errors(
4314 r#"pipeline t(task) {
4315 fn check(x: string | int | nil) {
4316 if x != nil {
4317 if type_of(x) == "int" {
4318 let i: int = x
4319 }
4320 }
4321 }
4322}"#,
4323 );
4324 assert!(errs.is_empty(), "got: {:?}", errs);
4325 }
4326
4327 #[test]
4328 fn test_match_narrows_arms() {
4329 let errs = errors(
4330 r#"pipeline t(task) {
4331 fn check(x: string | int) {
4332 match x {
4333 "hello" -> {
4334 let s: string = x
4335 }
4336 42 -> {
4337 let i: int = x
4338 }
4339 _ -> {}
4340 }
4341 }
4342}"#,
4343 );
4344 assert!(errs.is_empty(), "got: {:?}", errs);
4345 }
4346
4347 #[test]
4348 fn test_has_narrows_optional_field() {
4349 let errs = errors(
4350 r#"pipeline t(task) {
4351 fn check(x: {name?: string, age: int}) {
4352 if x.has("name") {
4353 let n: {name: string, age: int} = x
4354 }
4355 }
4356}"#,
4357 );
4358 assert!(errs.is_empty(), "got: {:?}", errs);
4359 }
4360
4361 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
4366 let mut lexer = Lexer::new(source);
4367 let tokens = lexer.tokenize().unwrap();
4368 let mut parser = Parser::new(tokens);
4369 let program = parser.parse().unwrap();
4370 TypeChecker::new().check_with_source(&program, source)
4371 }
4372
4373 #[test]
4374 fn test_fix_string_plus_int_literal() {
4375 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
4376 let diags = check_source_with_source(source);
4377 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4378 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
4379 let fix = fixable[0].fix.as_ref().unwrap();
4380 assert_eq!(fix.len(), 1);
4381 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
4382 }
4383
4384 #[test]
4385 fn test_fix_int_plus_string_literal() {
4386 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
4387 let diags = check_source_with_source(source);
4388 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4389 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
4390 let fix = fixable[0].fix.as_ref().unwrap();
4391 assert_eq!(fix[0].replacement, "\"${42}hello\"");
4392 }
4393
4394 #[test]
4395 fn test_fix_string_plus_variable() {
4396 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
4397 let diags = check_source_with_source(source);
4398 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4399 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
4400 let fix = fixable[0].fix.as_ref().unwrap();
4401 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
4402 }
4403
4404 #[test]
4405 fn test_no_fix_int_plus_int() {
4406 let source = "pipeline t(task) {\n let x: int = 5\n let y: float = 3.0\n let z = x - y\n log(z)\n}";
4408 let diags = check_source_with_source(source);
4409 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4410 assert!(
4411 fixable.is_empty(),
4412 "no fix expected for numeric ops, got: {fixable:?}"
4413 );
4414 }
4415
4416 #[test]
4417 fn test_no_fix_without_source() {
4418 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
4419 let diags = check_source(source);
4420 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4421 assert!(
4422 fixable.is_empty(),
4423 "without source, no fix should be generated"
4424 );
4425 }
4426
4427 #[test]
4430 fn test_union_exhaustive_match_no_warning() {
4431 let warns = warnings(
4432 r#"pipeline t(task) {
4433 let x: string | int | nil = nil
4434 match x {
4435 "hello" -> { log("s") }
4436 42 -> { log("i") }
4437 nil -> { log("n") }
4438 }
4439}"#,
4440 );
4441 let union_warns: Vec<_> = warns
4442 .iter()
4443 .filter(|w| w.contains("Non-exhaustive match on union"))
4444 .collect();
4445 assert!(union_warns.is_empty());
4446 }
4447
4448 #[test]
4449 fn test_union_non_exhaustive_match_warning() {
4450 let warns = warnings(
4451 r#"pipeline t(task) {
4452 let x: string | int | nil = nil
4453 match x {
4454 "hello" -> { log("s") }
4455 42 -> { log("i") }
4456 }
4457}"#,
4458 );
4459 let union_warns: Vec<_> = warns
4460 .iter()
4461 .filter(|w| w.contains("Non-exhaustive match on union"))
4462 .collect();
4463 assert_eq!(union_warns.len(), 1);
4464 assert!(union_warns[0].contains("nil"));
4465 }
4466
4467 #[test]
4470 fn test_nil_coalesce_non_union_preserves_left_type() {
4471 let errs = errors(
4473 r#"pipeline t(task) {
4474 let x: int = 42
4475 let y: int = x ?? 0
4476}"#,
4477 );
4478 assert!(errs.is_empty());
4479 }
4480
4481 #[test]
4482 fn test_nil_coalesce_nil_returns_right_type() {
4483 let errs = errors(
4484 r#"pipeline t(task) {
4485 let x: string = nil ?? "fallback"
4486}"#,
4487 );
4488 assert!(errs.is_empty());
4489 }
4490
4491 #[test]
4494 fn test_never_is_subtype_of_everything() {
4495 let tc = TypeChecker::new();
4496 let scope = TypeScope::new();
4497 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &TypeExpr::Never, &scope));
4498 assert!(tc.types_compatible(&TypeExpr::Named("int".into()), &TypeExpr::Never, &scope));
4499 assert!(tc.types_compatible(
4500 &TypeExpr::Union(vec![
4501 TypeExpr::Named("string".into()),
4502 TypeExpr::Named("nil".into()),
4503 ]),
4504 &TypeExpr::Never,
4505 &scope,
4506 ));
4507 }
4508
4509 #[test]
4510 fn test_nothing_is_subtype_of_never() {
4511 let tc = TypeChecker::new();
4512 let scope = TypeScope::new();
4513 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("string".into()), &scope));
4514 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("int".into()), &scope));
4515 }
4516
4517 #[test]
4518 fn test_never_never_compatible() {
4519 let tc = TypeChecker::new();
4520 let scope = TypeScope::new();
4521 assert!(tc.types_compatible(&TypeExpr::Never, &TypeExpr::Never, &scope));
4522 }
4523
4524 #[test]
4525 fn test_simplify_union_removes_never() {
4526 assert_eq!(
4527 simplify_union(vec![TypeExpr::Never, TypeExpr::Named("string".into())]),
4528 TypeExpr::Named("string".into()),
4529 );
4530 assert_eq!(
4531 simplify_union(vec![TypeExpr::Never, TypeExpr::Never]),
4532 TypeExpr::Never,
4533 );
4534 assert_eq!(
4535 simplify_union(vec![
4536 TypeExpr::Named("string".into()),
4537 TypeExpr::Never,
4538 TypeExpr::Named("int".into()),
4539 ]),
4540 TypeExpr::Union(vec![
4541 TypeExpr::Named("string".into()),
4542 TypeExpr::Named("int".into()),
4543 ]),
4544 );
4545 }
4546
4547 #[test]
4548 fn test_remove_from_union_exhausted_returns_never() {
4549 let result = remove_from_union(&[TypeExpr::Named("string".into())], "string");
4550 assert_eq!(result, Some(TypeExpr::Never));
4551 }
4552
4553 #[test]
4554 fn test_if_else_one_branch_throws_infers_other() {
4555 let errs = errors(
4557 r#"pipeline t(task) {
4558 fn foo(x: bool) -> int {
4559 let result: int = if x { 42 } else { throw "err" }
4560 return result
4561 }
4562}"#,
4563 );
4564 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4565 }
4566
4567 #[test]
4568 fn test_if_else_both_branches_throw_infers_never() {
4569 let errs = errors(
4571 r#"pipeline t(task) {
4572 fn foo(x: bool) -> string {
4573 let result: string = if x { throw "a" } else { throw "b" }
4574 return result
4575 }
4576}"#,
4577 );
4578 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4579 }
4580
4581 #[test]
4584 fn test_unreachable_after_return() {
4585 let warns = warnings(
4586 r#"pipeline t(task) {
4587 fn foo() -> int {
4588 return 1
4589 let x = 2
4590 }
4591}"#,
4592 );
4593 assert!(
4594 warns.iter().any(|w| w.contains("unreachable")),
4595 "expected unreachable warning: {warns:?}"
4596 );
4597 }
4598
4599 #[test]
4600 fn test_unreachable_after_throw() {
4601 let warns = warnings(
4602 r#"pipeline t(task) {
4603 fn foo() {
4604 throw "err"
4605 let x = 2
4606 }
4607}"#,
4608 );
4609 assert!(
4610 warns.iter().any(|w| w.contains("unreachable")),
4611 "expected unreachable warning: {warns:?}"
4612 );
4613 }
4614
4615 #[test]
4616 fn test_unreachable_after_composite_exit() {
4617 let warns = warnings(
4618 r#"pipeline t(task) {
4619 fn foo(x: bool) {
4620 if x { return 1 } else { throw "err" }
4621 let y = 2
4622 }
4623}"#,
4624 );
4625 assert!(
4626 warns.iter().any(|w| w.contains("unreachable")),
4627 "expected unreachable warning: {warns:?}"
4628 );
4629 }
4630
4631 #[test]
4632 fn test_no_unreachable_warning_when_reachable() {
4633 let warns = warnings(
4634 r#"pipeline t(task) {
4635 fn foo(x: bool) {
4636 if x { return 1 }
4637 let y = 2
4638 }
4639}"#,
4640 );
4641 assert!(
4642 !warns.iter().any(|w| w.contains("unreachable")),
4643 "unexpected unreachable warning: {warns:?}"
4644 );
4645 }
4646
4647 #[test]
4650 fn test_catch_typed_error_variable() {
4651 let errs = errors(
4653 r#"pipeline t(task) {
4654 enum AppError { NotFound, Timeout }
4655 try {
4656 throw AppError.NotFound
4657 } catch (e: AppError) {
4658 let x: AppError = e
4659 }
4660}"#,
4661 );
4662 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4663 }
4664
4665 #[test]
4668 fn test_unreachable_with_never_arg_no_error() {
4669 let errs = errors(
4671 r#"pipeline t(task) {
4672 fn foo(x: string | int) {
4673 if type_of(x) == "string" { return }
4674 if type_of(x) == "int" { return }
4675 unreachable(x)
4676 }
4677}"#,
4678 );
4679 assert!(
4680 !errs.iter().any(|e| e.contains("unreachable")),
4681 "unexpected unreachable error: {errs:?}"
4682 );
4683 }
4684
4685 #[test]
4686 fn test_unreachable_with_remaining_types_errors() {
4687 let errs = errors(
4689 r#"pipeline t(task) {
4690 fn foo(x: string | int | nil) {
4691 if type_of(x) == "string" { return }
4692 unreachable(x)
4693 }
4694}"#,
4695 );
4696 assert!(
4697 errs.iter()
4698 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
4699 "expected unreachable error about remaining types: {errs:?}"
4700 );
4701 }
4702
4703 #[test]
4704 fn test_unreachable_no_args_no_compile_error() {
4705 let errs = errors(
4706 r#"pipeline t(task) {
4707 fn foo() {
4708 unreachable()
4709 }
4710}"#,
4711 );
4712 assert!(
4713 !errs
4714 .iter()
4715 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
4716 "unreachable() with no args should not produce type error: {errs:?}"
4717 );
4718 }
4719
4720 #[test]
4721 fn test_never_type_annotation_parses() {
4722 let errs = errors(
4723 r#"pipeline t(task) {
4724 fn foo() -> never {
4725 throw "always throws"
4726 }
4727}"#,
4728 );
4729 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4730 }
4731
4732 #[test]
4733 fn test_format_type_never() {
4734 assert_eq!(format_type(&TypeExpr::Never), "never");
4735 }
4736}