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 if let Some(ref guard) = arm.guard {
929 self.check_node(guard, &mut arm_scope);
930 }
931 self.check_block(&arm.body, &mut arm_scope);
932 }
933 self.check_match_exhaustiveness(value, arms, scope, span);
934 }
935
936 Node::BinaryOp { op, left, right } => {
938 self.check_node(left, scope);
939 self.check_node(right, scope);
940 let lt = self.infer_type(left, scope);
942 let rt = self.infer_type(right, scope);
943 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
944 match op.as_str() {
945 "-" | "/" | "%" => {
946 let numeric = ["int", "float"];
947 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
948 self.error_at(
949 format!(
950 "Operator '{}' requires numeric operands, got {} and {}",
951 op, l, r
952 ),
953 span,
954 );
955 }
956 }
957 "*" => {
958 let numeric = ["int", "float"];
959 let is_numeric =
960 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
961 let is_string_repeat =
962 (l == "string" && r == "int") || (l == "int" && r == "string");
963 if !is_numeric && !is_string_repeat {
964 self.error_at(
965 format!(
966 "Operator '*' requires numeric operands or string * int, got {} and {}",
967 l, r
968 ),
969 span,
970 );
971 }
972 }
973 "+" => {
974 let valid = matches!(
975 (l.as_str(), r.as_str()),
976 ("int" | "float", "int" | "float")
977 | ("string", "string")
978 | ("list", "list")
979 | ("dict", "dict")
980 );
981 if !valid {
982 let msg =
983 format!("Operator '+' is not valid for types {} and {}", l, r);
984 let fix = if l == "string" || r == "string" {
986 self.build_interpolation_fix(left, right, l == "string", span)
987 } else {
988 None
989 };
990 if let Some(fix) = fix {
991 self.error_at_with_fix(msg, span, fix);
992 } else {
993 self.error_at(msg, span);
994 }
995 }
996 }
997 "<" | ">" | "<=" | ">=" => {
998 let comparable = ["int", "float", "string"];
999 if !comparable.contains(&l.as_str())
1000 || !comparable.contains(&r.as_str())
1001 {
1002 self.warning_at(
1003 format!(
1004 "Comparison '{}' may not be meaningful for types {} and {}",
1005 op, l, r
1006 ),
1007 span,
1008 );
1009 } else if (l == "string") != (r == "string") {
1010 self.warning_at(
1011 format!(
1012 "Comparing {} with {} using '{}' may give unexpected results",
1013 l, r, op
1014 ),
1015 span,
1016 );
1017 }
1018 }
1019 _ => {}
1020 }
1021 }
1022 }
1023 Node::UnaryOp { operand, .. } => {
1024 self.check_node(operand, scope);
1025 }
1026 Node::MethodCall {
1027 object,
1028 method,
1029 args,
1030 ..
1031 }
1032 | Node::OptionalMethodCall {
1033 object,
1034 method,
1035 args,
1036 ..
1037 } => {
1038 self.check_node(object, scope);
1039 for arg in args {
1040 self.check_node(arg, scope);
1041 }
1042 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
1046 if scope.is_generic_type_param(&type_name) {
1047 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
1048 if let Some(iface_methods) = scope.get_interface(iface_name) {
1049 let has_method = iface_methods.iter().any(|m| m.name == *method);
1050 if !has_method {
1051 self.warning_at(
1052 format!(
1053 "Method '{}' not found in interface '{}' (constraint on '{}')",
1054 method, iface_name, type_name
1055 ),
1056 span,
1057 );
1058 }
1059 }
1060 }
1061 }
1062 }
1063 }
1064 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1065 self.check_node(object, scope);
1066 }
1067 Node::SubscriptAccess { object, index } => {
1068 self.check_node(object, scope);
1069 self.check_node(index, scope);
1070 }
1071 Node::SliceAccess { object, start, end } => {
1072 self.check_node(object, scope);
1073 if let Some(s) = start {
1074 self.check_node(s, scope);
1075 }
1076 if let Some(e) = end {
1077 self.check_node(e, scope);
1078 }
1079 }
1080
1081 Node::Ternary {
1083 condition,
1084 true_expr,
1085 false_expr,
1086 } => {
1087 self.check_node(condition, scope);
1088 let refs = Self::extract_refinements(condition, scope);
1089
1090 let mut true_scope = scope.child();
1091 apply_refinements(&mut true_scope, &refs.truthy);
1092 self.check_node(true_expr, &mut true_scope);
1093
1094 let mut false_scope = scope.child();
1095 apply_refinements(&mut false_scope, &refs.falsy);
1096 self.check_node(false_expr, &mut false_scope);
1097 }
1098
1099 Node::ThrowStmt { value } => {
1100 self.check_node(value, scope);
1101 }
1102
1103 Node::GuardStmt {
1104 condition,
1105 else_body,
1106 } => {
1107 self.check_node(condition, scope);
1108 let refs = Self::extract_refinements(condition, scope);
1109
1110 let mut else_scope = scope.child();
1111 apply_refinements(&mut else_scope, &refs.falsy);
1112 self.check_block(else_body, &mut else_scope);
1113
1114 apply_refinements(scope, &refs.truthy);
1117 }
1118
1119 Node::SpawnExpr { body } => {
1120 let mut spawn_scope = scope.child();
1121 self.check_block(body, &mut spawn_scope);
1122 }
1123
1124 Node::Parallel {
1125 mode,
1126 expr,
1127 variable,
1128 body,
1129 } => {
1130 self.check_node(expr, scope);
1131 let mut par_scope = scope.child();
1132 if let Some(var) = variable {
1133 let var_type = match mode {
1134 ParallelMode::Count => Some(TypeExpr::Named("int".into())),
1135 ParallelMode::Each | ParallelMode::Settle => {
1136 match self.infer_type(expr, scope) {
1137 Some(TypeExpr::List(inner)) => Some(*inner),
1138 _ => None,
1139 }
1140 }
1141 };
1142 par_scope.define_var(var, var_type);
1143 }
1144 self.check_block(body, &mut par_scope);
1145 }
1146
1147 Node::SelectExpr {
1148 cases,
1149 timeout,
1150 default_body,
1151 } => {
1152 for case in cases {
1153 self.check_node(&case.channel, scope);
1154 let mut case_scope = scope.child();
1155 case_scope.define_var(&case.variable, None);
1156 self.check_block(&case.body, &mut case_scope);
1157 }
1158 if let Some((dur, body)) = timeout {
1159 self.check_node(dur, scope);
1160 let mut timeout_scope = scope.child();
1161 self.check_block(body, &mut timeout_scope);
1162 }
1163 if let Some(body) = default_body {
1164 let mut default_scope = scope.child();
1165 self.check_block(body, &mut default_scope);
1166 }
1167 }
1168
1169 Node::DeadlineBlock { duration, body } => {
1170 self.check_node(duration, scope);
1171 let mut block_scope = scope.child();
1172 self.check_block(body, &mut block_scope);
1173 }
1174
1175 Node::MutexBlock { body } | Node::DeferStmt { body } => {
1176 let mut block_scope = scope.child();
1177 self.check_block(body, &mut block_scope);
1178 }
1179
1180 Node::Retry { count, body } => {
1181 self.check_node(count, scope);
1182 let mut retry_scope = scope.child();
1183 self.check_block(body, &mut retry_scope);
1184 }
1185
1186 Node::Closure { params, body, .. } => {
1187 let mut closure_scope = scope.child();
1188 for p in params {
1189 closure_scope.define_var(&p.name, p.type_expr.clone());
1190 }
1191 self.check_block(body, &mut closure_scope);
1192 }
1193
1194 Node::ListLiteral(elements) => {
1195 for elem in elements {
1196 self.check_node(elem, scope);
1197 }
1198 }
1199
1200 Node::DictLiteral(entries) => {
1201 for entry in entries {
1202 self.check_node(&entry.key, scope);
1203 self.check_node(&entry.value, scope);
1204 }
1205 }
1206
1207 Node::RangeExpr { start, end, .. } => {
1208 self.check_node(start, scope);
1209 self.check_node(end, scope);
1210 }
1211
1212 Node::Spread(inner) => {
1213 self.check_node(inner, scope);
1214 }
1215
1216 Node::Block(stmts) => {
1217 let mut block_scope = scope.child();
1218 self.check_block(stmts, &mut block_scope);
1219 }
1220
1221 Node::YieldExpr { value } => {
1222 if let Some(v) = value {
1223 self.check_node(v, scope);
1224 }
1225 }
1226
1227 Node::StructConstruct {
1229 struct_name,
1230 fields,
1231 } => {
1232 for entry in fields {
1233 self.check_node(&entry.key, scope);
1234 self.check_node(&entry.value, scope);
1235 }
1236 if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
1237 for entry in fields {
1239 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1240 if !declared_fields.iter().any(|(name, _)| name == key) {
1241 self.warning_at(
1242 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1243 entry.key.span,
1244 );
1245 }
1246 }
1247 }
1248 let provided: Vec<String> = fields
1250 .iter()
1251 .filter_map(|e| match &e.key.node {
1252 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1253 _ => None,
1254 })
1255 .collect();
1256 for (name, _) in &declared_fields {
1257 if !provided.contains(name) {
1258 self.warning_at(
1259 format!(
1260 "Missing field '{}' in struct '{}' construction",
1261 name, struct_name
1262 ),
1263 span,
1264 );
1265 }
1266 }
1267 }
1268 }
1269
1270 Node::EnumConstruct {
1272 enum_name,
1273 variant,
1274 args,
1275 } => {
1276 for arg in args {
1277 self.check_node(arg, scope);
1278 }
1279 if let Some(variants) = scope.get_enum(enum_name) {
1280 if !variants.contains(variant) {
1281 self.warning_at(
1282 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1283 span,
1284 );
1285 }
1286 }
1287 }
1288
1289 Node::InterpolatedString(_) => {}
1291
1292 Node::StringLiteral(_)
1294 | Node::RawStringLiteral(_)
1295 | Node::IntLiteral(_)
1296 | Node::FloatLiteral(_)
1297 | Node::BoolLiteral(_)
1298 | Node::NilLiteral
1299 | Node::Identifier(_)
1300 | Node::DurationLiteral(_)
1301 | Node::BreakStmt
1302 | Node::ContinueStmt
1303 | Node::ReturnStmt { value: None }
1304 | Node::ImportDecl { .. }
1305 | Node::SelectiveImport { .. } => {}
1306
1307 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1310 let mut decl_scope = scope.child();
1311 self.check_block(body, &mut decl_scope);
1312 }
1313 }
1314 }
1315
1316 fn check_fn_body(
1317 &mut self,
1318 type_params: &[TypeParam],
1319 params: &[TypedParam],
1320 return_type: &Option<TypeExpr>,
1321 body: &[SNode],
1322 where_clauses: &[WhereClause],
1323 ) {
1324 let mut fn_scope = self.scope.child();
1325 for tp in type_params {
1328 fn_scope.generic_type_params.insert(tp.name.clone());
1329 }
1330 for wc in where_clauses {
1332 fn_scope
1333 .where_constraints
1334 .insert(wc.type_name.clone(), wc.bound.clone());
1335 }
1336 for param in params {
1337 fn_scope.define_var(¶m.name, param.type_expr.clone());
1338 if let Some(default) = ¶m.default_value {
1339 self.check_node(default, &mut fn_scope);
1340 }
1341 }
1342 let ret_scope_base = if return_type.is_some() {
1345 Some(fn_scope.child())
1346 } else {
1347 None
1348 };
1349
1350 self.check_block(body, &mut fn_scope);
1351
1352 if let Some(ret_type) = return_type {
1354 let mut ret_scope = ret_scope_base.unwrap();
1355 for stmt in body {
1356 self.check_return_type(stmt, ret_type, &mut ret_scope);
1357 }
1358 }
1359 }
1360
1361 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1362 let span = snode.span;
1363 match &snode.node {
1364 Node::ReturnStmt { value: Some(val) } => {
1365 let inferred = self.infer_type(val, scope);
1366 if let Some(actual) = &inferred {
1367 if !self.types_compatible(expected, actual, scope) {
1368 self.error_at(
1369 format!(
1370 "Return type mismatch: expected {}, got {}",
1371 format_type(expected),
1372 format_type(actual)
1373 ),
1374 span,
1375 );
1376 }
1377 }
1378 }
1379 Node::IfElse {
1380 condition,
1381 then_body,
1382 else_body,
1383 } => {
1384 let refs = Self::extract_refinements(condition, scope);
1385 let mut then_scope = scope.child();
1386 apply_refinements(&mut then_scope, &refs.truthy);
1387 for stmt in then_body {
1388 self.check_return_type(stmt, expected, &mut then_scope);
1389 }
1390 if let Some(else_body) = else_body {
1391 let mut else_scope = scope.child();
1392 apply_refinements(&mut else_scope, &refs.falsy);
1393 for stmt in else_body {
1394 self.check_return_type(stmt, expected, &mut else_scope);
1395 }
1396 if Self::block_definitely_exits(then_body)
1398 && !Self::block_definitely_exits(else_body)
1399 {
1400 apply_refinements(scope, &refs.falsy);
1401 } else if Self::block_definitely_exits(else_body)
1402 && !Self::block_definitely_exits(then_body)
1403 {
1404 apply_refinements(scope, &refs.truthy);
1405 }
1406 } else {
1407 if Self::block_definitely_exits(then_body) {
1409 apply_refinements(scope, &refs.falsy);
1410 }
1411 }
1412 }
1413 _ => {}
1414 }
1415 }
1416
1417 fn satisfies_interface(
1423 &self,
1424 type_name: &str,
1425 interface_name: &str,
1426 scope: &TypeScope,
1427 ) -> bool {
1428 self.interface_mismatch_reason(type_name, interface_name, scope)
1429 .is_none()
1430 }
1431
1432 fn interface_mismatch_reason(
1435 &self,
1436 type_name: &str,
1437 interface_name: &str,
1438 scope: &TypeScope,
1439 ) -> Option<String> {
1440 let interface_methods = match scope.get_interface(interface_name) {
1441 Some(methods) => methods,
1442 None => return Some(format!("interface '{}' not found", interface_name)),
1443 };
1444 let impl_methods = match scope.get_impl_methods(type_name) {
1445 Some(methods) => methods,
1446 None => {
1447 if interface_methods.is_empty() {
1448 return None;
1449 }
1450 let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1451 return Some(format!("missing method(s): {}", names.join(", ")));
1452 }
1453 };
1454 for iface_method in interface_methods {
1455 let iface_params: Vec<_> = iface_method
1456 .params
1457 .iter()
1458 .filter(|p| p.name != "self")
1459 .collect();
1460 let iface_param_count = iface_params.len();
1461 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1462 let impl_method = match matching_impl {
1463 Some(m) => m,
1464 None => {
1465 return Some(format!("missing method '{}'", iface_method.name));
1466 }
1467 };
1468 if impl_method.param_count != iface_param_count {
1469 return Some(format!(
1470 "method '{}' has {} parameter(s), expected {}",
1471 iface_method.name, impl_method.param_count, iface_param_count
1472 ));
1473 }
1474 for (i, iface_param) in iface_params.iter().enumerate() {
1476 if let (Some(expected), Some(actual)) = (
1477 &iface_param.type_expr,
1478 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1479 ) {
1480 if !self.types_compatible(expected, actual, scope) {
1481 return Some(format!(
1482 "method '{}' parameter {} has type '{}', expected '{}'",
1483 iface_method.name,
1484 i + 1,
1485 format_type(actual),
1486 format_type(expected),
1487 ));
1488 }
1489 }
1490 }
1491 if let (Some(expected_ret), Some(actual_ret)) =
1493 (&iface_method.return_type, &impl_method.return_type)
1494 {
1495 if !self.types_compatible(expected_ret, actual_ret, scope) {
1496 return Some(format!(
1497 "method '{}' returns '{}', expected '{}'",
1498 iface_method.name,
1499 format_type(actual_ret),
1500 format_type(expected_ret),
1501 ));
1502 }
1503 }
1504 }
1505 None
1506 }
1507
1508 fn bind_type_param(
1509 param_name: &str,
1510 concrete: &TypeExpr,
1511 bindings: &mut BTreeMap<String, TypeExpr>,
1512 ) -> Result<(), String> {
1513 if let Some(existing) = bindings.get(param_name) {
1514 if existing != concrete {
1515 return Err(format!(
1516 "type parameter '{}' was inferred as both {} and {}",
1517 param_name,
1518 format_type(existing),
1519 format_type(concrete)
1520 ));
1521 }
1522 return Ok(());
1523 }
1524 bindings.insert(param_name.to_string(), concrete.clone());
1525 Ok(())
1526 }
1527
1528 fn extract_type_bindings(
1531 param_type: &TypeExpr,
1532 arg_type: &TypeExpr,
1533 type_params: &std::collections::BTreeSet<String>,
1534 bindings: &mut BTreeMap<String, TypeExpr>,
1535 ) -> Result<(), String> {
1536 match (param_type, arg_type) {
1537 (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
1538 Self::bind_type_param(param_name, concrete, bindings)
1539 }
1540 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1541 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
1542 }
1543 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1544 Self::extract_type_bindings(pk, ak, type_params, bindings)?;
1545 Self::extract_type_bindings(pv, av, type_params, bindings)
1546 }
1547 (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
1548 for param_field in param_fields {
1549 if let Some(arg_field) = arg_fields
1550 .iter()
1551 .find(|field| field.name == param_field.name)
1552 {
1553 Self::extract_type_bindings(
1554 ¶m_field.type_expr,
1555 &arg_field.type_expr,
1556 type_params,
1557 bindings,
1558 )?;
1559 }
1560 }
1561 Ok(())
1562 }
1563 (
1564 TypeExpr::FnType {
1565 params: p_params,
1566 return_type: p_ret,
1567 },
1568 TypeExpr::FnType {
1569 params: a_params,
1570 return_type: a_ret,
1571 },
1572 ) => {
1573 for (param, arg) in p_params.iter().zip(a_params.iter()) {
1574 Self::extract_type_bindings(param, arg, type_params, bindings)?;
1575 }
1576 Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
1577 }
1578 _ => Ok(()),
1579 }
1580 }
1581
1582 fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
1583 match ty {
1584 TypeExpr::Named(name) => bindings
1585 .get(name)
1586 .cloned()
1587 .unwrap_or_else(|| TypeExpr::Named(name.clone())),
1588 TypeExpr::Union(items) => TypeExpr::Union(
1589 items
1590 .iter()
1591 .map(|item| Self::apply_type_bindings(item, bindings))
1592 .collect(),
1593 ),
1594 TypeExpr::Shape(fields) => TypeExpr::Shape(
1595 fields
1596 .iter()
1597 .map(|field| ShapeField {
1598 name: field.name.clone(),
1599 type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
1600 optional: field.optional,
1601 })
1602 .collect(),
1603 ),
1604 TypeExpr::List(inner) => {
1605 TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
1606 }
1607 TypeExpr::DictType(key, value) => TypeExpr::DictType(
1608 Box::new(Self::apply_type_bindings(key, bindings)),
1609 Box::new(Self::apply_type_bindings(value, bindings)),
1610 ),
1611 TypeExpr::FnType {
1612 params,
1613 return_type,
1614 } => TypeExpr::FnType {
1615 params: params
1616 .iter()
1617 .map(|param| Self::apply_type_bindings(param, bindings))
1618 .collect(),
1619 return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
1620 },
1621 TypeExpr::Never => TypeExpr::Never,
1622 }
1623 }
1624
1625 fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
1626 let mut inferred: Option<TypeExpr> = None;
1627 for item in items {
1628 let Some(item_type) = self.infer_type(item, scope) else {
1629 return TypeExpr::Named("list".into());
1630 };
1631 inferred = Some(match inferred {
1632 None => item_type,
1633 Some(current) if current == item_type => current,
1634 Some(TypeExpr::Union(mut members)) => {
1635 if !members.contains(&item_type) {
1636 members.push(item_type);
1637 }
1638 TypeExpr::Union(members)
1639 }
1640 Some(current) => TypeExpr::Union(vec![current, item_type]),
1641 });
1642 }
1643 inferred
1644 .map(|item_type| TypeExpr::List(Box::new(item_type)))
1645 .unwrap_or_else(|| TypeExpr::Named("list".into()))
1646 }
1647
1648 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1650 match &condition.node {
1651 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1653 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1654 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1655 return nil_ref;
1656 }
1657 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1658 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1659 return typeof_ref;
1660 }
1661 Refinements::empty()
1662 }
1663
1664 Node::BinaryOp { op, left, right } if op == "&&" => {
1666 let left_ref = Self::extract_refinements(left, scope);
1667 let right_ref = Self::extract_refinements(right, scope);
1668 let mut truthy = left_ref.truthy;
1669 truthy.extend(right_ref.truthy);
1670 Refinements {
1671 truthy,
1672 falsy: vec![],
1673 }
1674 }
1675
1676 Node::BinaryOp { op, left, right } if op == "||" => {
1678 let left_ref = Self::extract_refinements(left, scope);
1679 let right_ref = Self::extract_refinements(right, scope);
1680 let mut falsy = left_ref.falsy;
1681 falsy.extend(right_ref.falsy);
1682 Refinements {
1683 truthy: vec![],
1684 falsy,
1685 }
1686 }
1687
1688 Node::UnaryOp { op, operand } if op == "!" => {
1690 Self::extract_refinements(operand, scope).inverted()
1691 }
1692
1693 Node::Identifier(name) => {
1695 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1696 if members
1697 .iter()
1698 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1699 {
1700 if let Some(narrowed) = remove_from_union(members, "nil") {
1701 return Refinements {
1702 truthy: vec![(name.clone(), Some(narrowed))],
1703 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1704 };
1705 }
1706 }
1707 }
1708 Refinements::empty()
1709 }
1710
1711 Node::MethodCall {
1713 object,
1714 method,
1715 args,
1716 } if method == "has" && args.len() == 1 => {
1717 Self::extract_has_refinements(object, args, scope)
1718 }
1719
1720 Node::FunctionCall { name, args }
1721 if (name == "schema_is" || name == "is_type") && args.len() == 2 =>
1722 {
1723 Self::extract_schema_refinements(args, scope)
1724 }
1725
1726 _ => Refinements::empty(),
1727 }
1728 }
1729
1730 fn extract_nil_refinements(
1732 op: &str,
1733 left: &SNode,
1734 right: &SNode,
1735 scope: &TypeScope,
1736 ) -> Refinements {
1737 let var_node = if matches!(right.node, Node::NilLiteral) {
1738 left
1739 } else if matches!(left.node, Node::NilLiteral) {
1740 right
1741 } else {
1742 return Refinements::empty();
1743 };
1744
1745 if let Node::Identifier(name) = &var_node.node {
1746 let var_type = scope.get_var(name).cloned().flatten();
1747 match var_type {
1748 Some(TypeExpr::Union(ref members)) => {
1749 if let Some(narrowed) = remove_from_union(members, "nil") {
1750 let neq_refs = Refinements {
1751 truthy: vec![(name.clone(), Some(narrowed))],
1752 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1753 };
1754 return if op == "!=" {
1755 neq_refs
1756 } else {
1757 neq_refs.inverted()
1758 };
1759 }
1760 }
1761 Some(TypeExpr::Named(ref n)) if n == "nil" => {
1762 let eq_refs = Refinements {
1764 truthy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1765 falsy: vec![(name.clone(), Some(TypeExpr::Never))],
1766 };
1767 return if op == "==" {
1768 eq_refs
1769 } else {
1770 eq_refs.inverted()
1771 };
1772 }
1773 _ => {}
1774 }
1775 }
1776 Refinements::empty()
1777 }
1778
1779 fn extract_typeof_refinements(
1781 op: &str,
1782 left: &SNode,
1783 right: &SNode,
1784 scope: &TypeScope,
1785 ) -> Refinements {
1786 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1787 (extract_type_of_var(left), &right.node)
1788 {
1789 (var, tn.clone())
1790 } else if let (Node::StringLiteral(tn), Some(var)) =
1791 (&left.node, extract_type_of_var(right))
1792 {
1793 (var, tn.clone())
1794 } else {
1795 return Refinements::empty();
1796 };
1797
1798 const KNOWN_TYPES: &[&str] = &[
1799 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1800 ];
1801 if !KNOWN_TYPES.contains(&type_name.as_str()) {
1802 return Refinements::empty();
1803 }
1804
1805 let var_type = scope.get_var(&var_name).cloned().flatten();
1806 match var_type {
1807 Some(TypeExpr::Union(ref members)) => {
1808 let narrowed = narrow_to_single(members, &type_name);
1809 let remaining = remove_from_union(members, &type_name);
1810 if narrowed.is_some() || remaining.is_some() {
1811 let eq_refs = Refinements {
1812 truthy: narrowed
1813 .map(|n| vec![(var_name.clone(), Some(n))])
1814 .unwrap_or_default(),
1815 falsy: remaining
1816 .map(|r| vec![(var_name.clone(), Some(r))])
1817 .unwrap_or_default(),
1818 };
1819 return if op == "==" {
1820 eq_refs
1821 } else {
1822 eq_refs.inverted()
1823 };
1824 }
1825 }
1826 Some(TypeExpr::Named(ref n)) if n == &type_name => {
1827 let eq_refs = Refinements {
1830 truthy: vec![(var_name.clone(), Some(TypeExpr::Named(type_name)))],
1831 falsy: vec![(var_name.clone(), Some(TypeExpr::Never))],
1832 };
1833 return if op == "==" {
1834 eq_refs
1835 } else {
1836 eq_refs.inverted()
1837 };
1838 }
1839 _ => {}
1840 }
1841 Refinements::empty()
1842 }
1843
1844 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1846 if let Node::Identifier(var_name) = &object.node {
1847 if let Node::StringLiteral(key) = &args[0].node {
1848 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1849 if fields.iter().any(|f| f.name == *key && f.optional) {
1850 let narrowed_fields: Vec<ShapeField> = fields
1851 .iter()
1852 .map(|f| {
1853 if f.name == *key {
1854 ShapeField {
1855 name: f.name.clone(),
1856 type_expr: f.type_expr.clone(),
1857 optional: false,
1858 }
1859 } else {
1860 f.clone()
1861 }
1862 })
1863 .collect();
1864 return Refinements {
1865 truthy: vec![(
1866 var_name.clone(),
1867 Some(TypeExpr::Shape(narrowed_fields)),
1868 )],
1869 falsy: vec![],
1870 };
1871 }
1872 }
1873 }
1874 }
1875 Refinements::empty()
1876 }
1877
1878 fn extract_schema_refinements(args: &[SNode], scope: &TypeScope) -> Refinements {
1879 let Node::Identifier(var_name) = &args[0].node else {
1880 return Refinements::empty();
1881 };
1882 let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) else {
1883 return Refinements::empty();
1884 };
1885 let Some(Some(var_type)) = scope.get_var(var_name).cloned() else {
1886 return Refinements::empty();
1887 };
1888
1889 let truthy = intersect_types(&var_type, &schema_type)
1890 .map(|ty| vec![(var_name.clone(), Some(ty))])
1891 .unwrap_or_default();
1892 let falsy = subtract_type(&var_type, &schema_type)
1893 .map(|ty| vec![(var_name.clone(), Some(ty))])
1894 .unwrap_or_default();
1895
1896 Refinements { truthy, falsy }
1897 }
1898
1899 fn block_definitely_exits(stmts: &[SNode]) -> bool {
1901 block_definitely_exits(stmts)
1902 }
1903
1904 fn check_match_exhaustiveness(
1905 &mut self,
1906 value: &SNode,
1907 arms: &[MatchArm],
1908 scope: &TypeScope,
1909 span: Span,
1910 ) {
1911 let enum_name = match &value.node {
1913 Node::PropertyAccess { object, property } if property == "variant" => {
1914 match self.infer_type(object, scope) {
1916 Some(TypeExpr::Named(name)) => {
1917 if scope.get_enum(&name).is_some() {
1918 Some(name)
1919 } else {
1920 None
1921 }
1922 }
1923 _ => None,
1924 }
1925 }
1926 _ => {
1927 match self.infer_type(value, scope) {
1929 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1930 _ => None,
1931 }
1932 }
1933 };
1934
1935 let Some(enum_name) = enum_name else {
1936 self.check_match_exhaustiveness_union(value, arms, scope, span);
1938 return;
1939 };
1940 let Some(variants) = scope.get_enum(&enum_name) else {
1941 return;
1942 };
1943
1944 let mut covered: Vec<String> = Vec::new();
1946 let mut has_wildcard = false;
1947
1948 for arm in arms {
1949 match &arm.pattern.node {
1950 Node::StringLiteral(s) => covered.push(s.clone()),
1952 Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1954 has_wildcard = true;
1955 }
1956 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1958 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1960 _ => {
1961 has_wildcard = true;
1963 }
1964 }
1965 }
1966
1967 if has_wildcard {
1968 return;
1969 }
1970
1971 let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1972 if !missing.is_empty() {
1973 let missing_str = missing
1974 .iter()
1975 .map(|s| format!("\"{}\"", s))
1976 .collect::<Vec<_>>()
1977 .join(", ");
1978 self.warning_at(
1979 format!(
1980 "Non-exhaustive match on enum {}: missing variants {}",
1981 enum_name, missing_str
1982 ),
1983 span,
1984 );
1985 }
1986 }
1987
1988 fn check_match_exhaustiveness_union(
1990 &mut self,
1991 value: &SNode,
1992 arms: &[MatchArm],
1993 scope: &TypeScope,
1994 span: Span,
1995 ) {
1996 let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
1997 return;
1998 };
1999 if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
2001 return;
2002 }
2003
2004 let mut has_wildcard = false;
2005 let mut covered_types: Vec<String> = Vec::new();
2006
2007 for arm in arms {
2008 match &arm.pattern.node {
2009 Node::NilLiteral => covered_types.push("nil".into()),
2012 Node::BoolLiteral(_) => {
2013 if !covered_types.contains(&"bool".into()) {
2014 covered_types.push("bool".into());
2015 }
2016 }
2017 Node::IntLiteral(_) => {
2018 if !covered_types.contains(&"int".into()) {
2019 covered_types.push("int".into());
2020 }
2021 }
2022 Node::FloatLiteral(_) => {
2023 if !covered_types.contains(&"float".into()) {
2024 covered_types.push("float".into());
2025 }
2026 }
2027 Node::StringLiteral(_) => {
2028 if !covered_types.contains(&"string".into()) {
2029 covered_types.push("string".into());
2030 }
2031 }
2032 Node::Identifier(name) if name == "_" => {
2033 has_wildcard = true;
2034 }
2035 _ => {
2036 has_wildcard = true;
2037 }
2038 }
2039 }
2040
2041 if has_wildcard {
2042 return;
2043 }
2044
2045 let type_names: Vec<&str> = members
2046 .iter()
2047 .filter_map(|m| match m {
2048 TypeExpr::Named(n) => Some(n.as_str()),
2049 _ => None,
2050 })
2051 .collect();
2052 let missing: Vec<&&str> = type_names
2053 .iter()
2054 .filter(|t| !covered_types.iter().any(|c| c == **t))
2055 .collect();
2056 if !missing.is_empty() {
2057 let missing_str = missing
2058 .iter()
2059 .map(|s| s.to_string())
2060 .collect::<Vec<_>>()
2061 .join(", ");
2062 self.warning_at(
2063 format!(
2064 "Non-exhaustive match on union type: missing {}",
2065 missing_str
2066 ),
2067 span,
2068 );
2069 }
2070 }
2071
2072 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
2073 if name == "unreachable" {
2076 if let Some(arg) = args.first() {
2077 if matches!(&arg.node, Node::Identifier(_)) {
2078 let arg_type = self.infer_type(arg, scope);
2079 if let Some(ref ty) = arg_type {
2080 if !matches!(ty, TypeExpr::Never) {
2081 self.error_at(
2082 format!(
2083 "unreachable() argument has type `{}` — not all cases are handled",
2084 format_type(ty)
2085 ),
2086 span,
2087 );
2088 }
2089 }
2090 }
2091 }
2092 for arg in args {
2093 self.check_node(arg, scope);
2094 }
2095 return;
2096 }
2097
2098 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
2100 if let Some(sig) = scope.get_fn(name).cloned() {
2101 if !has_spread
2102 && !is_builtin(name)
2103 && !sig.has_rest
2104 && (args.len() < sig.required_params || args.len() > sig.params.len())
2105 {
2106 let expected = if sig.required_params == sig.params.len() {
2107 format!("{}", sig.params.len())
2108 } else {
2109 format!("{}-{}", sig.required_params, sig.params.len())
2110 };
2111 self.warning_at(
2112 format!(
2113 "Function '{}' expects {} arguments, got {}",
2114 name,
2115 expected,
2116 args.len()
2117 ),
2118 span,
2119 );
2120 }
2121 let call_scope = if sig.type_param_names.is_empty() {
2124 scope.clone()
2125 } else {
2126 let mut s = scope.child();
2127 for tp_name in &sig.type_param_names {
2128 s.generic_type_params.insert(tp_name.clone());
2129 }
2130 s
2131 };
2132 let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
2133 let type_param_set: std::collections::BTreeSet<String> =
2134 sig.type_param_names.iter().cloned().collect();
2135 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2136 if let Some(param_ty) = param_type {
2137 if let Some(arg_ty) = self.infer_type(arg, scope) {
2138 if let Err(message) = Self::extract_type_bindings(
2139 param_ty,
2140 &arg_ty,
2141 &type_param_set,
2142 &mut type_bindings,
2143 ) {
2144 self.error_at(message, arg.span);
2145 }
2146 }
2147 }
2148 }
2149 for (i, (arg, (param_name, param_type))) in
2150 args.iter().zip(sig.params.iter()).enumerate()
2151 {
2152 if let Some(expected) = param_type {
2153 let actual = self.infer_type(arg, scope);
2154 if let Some(actual) = &actual {
2155 let expected = Self::apply_type_bindings(expected, &type_bindings);
2156 if !self.types_compatible(&expected, actual, &call_scope) {
2157 self.error_at(
2158 format!(
2159 "Argument {} ('{}'): expected {}, got {}",
2160 i + 1,
2161 param_name,
2162 format_type(&expected),
2163 format_type(actual)
2164 ),
2165 arg.span,
2166 );
2167 }
2168 }
2169 }
2170 }
2171 if !sig.where_clauses.is_empty() {
2172 for (type_param, bound) in &sig.where_clauses {
2173 if let Some(concrete_type) = type_bindings.get(type_param) {
2174 let concrete_name = format_type(concrete_type);
2175 if let Some(reason) =
2176 self.interface_mismatch_reason(&concrete_name, bound, scope)
2177 {
2178 self.error_at(
2179 format!(
2180 "Type '{}' does not satisfy interface '{}': {} \
2181 (required by constraint `where {}: {}`)",
2182 concrete_name, bound, reason, type_param, bound
2183 ),
2184 span,
2185 );
2186 }
2187 }
2188 }
2189 }
2190 }
2191 for arg in args {
2193 self.check_node(arg, scope);
2194 }
2195 }
2196
2197 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
2199 match &snode.node {
2200 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
2201 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
2202 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
2203 Some(TypeExpr::Named("string".into()))
2204 }
2205 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
2206 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
2207 Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
2208 Node::DictLiteral(entries) => {
2209 let mut fields = Vec::new();
2211 for entry in entries {
2212 let key = match &entry.key.node {
2213 Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
2214 _ => return Some(TypeExpr::Named("dict".into())),
2215 };
2216 let val_type = self
2217 .infer_type(&entry.value, scope)
2218 .unwrap_or(TypeExpr::Named("nil".into()));
2219 fields.push(ShapeField {
2220 name: key,
2221 type_expr: val_type,
2222 optional: false,
2223 });
2224 }
2225 if !fields.is_empty() {
2226 Some(TypeExpr::Shape(fields))
2227 } else {
2228 Some(TypeExpr::Named("dict".into()))
2229 }
2230 }
2231 Node::Closure { params, body, .. } => {
2232 let all_typed = params.iter().all(|p| p.type_expr.is_some());
2234 if all_typed && !params.is_empty() {
2235 let param_types: Vec<TypeExpr> =
2236 params.iter().filter_map(|p| p.type_expr.clone()).collect();
2237 let ret = body.last().and_then(|last| self.infer_type(last, scope));
2239 if let Some(ret_type) = ret {
2240 return Some(TypeExpr::FnType {
2241 params: param_types,
2242 return_type: Box::new(ret_type),
2243 });
2244 }
2245 }
2246 Some(TypeExpr::Named("closure".into()))
2247 }
2248
2249 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
2250
2251 Node::FunctionCall { name, args } => {
2252 if scope.get_struct(name).is_some() {
2254 return Some(TypeExpr::Named(name.clone()));
2255 }
2256 if let Some(sig) = scope.get_fn(name) {
2258 let mut return_type = sig.return_type.clone();
2259 if let Some(ty) = return_type.take() {
2260 if sig.type_param_names.is_empty() {
2261 return Some(ty);
2262 }
2263 let mut bindings = BTreeMap::new();
2264 let type_param_set: std::collections::BTreeSet<String> =
2265 sig.type_param_names.iter().cloned().collect();
2266 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2267 if let Some(param_ty) = param_type {
2268 if let Some(arg_ty) = self.infer_type(arg, scope) {
2269 let _ = Self::extract_type_bindings(
2270 param_ty,
2271 &arg_ty,
2272 &type_param_set,
2273 &mut bindings,
2274 );
2275 }
2276 }
2277 }
2278 return Some(Self::apply_type_bindings(&ty, &bindings));
2279 }
2280 return None;
2281 }
2282 builtin_return_type(name)
2284 }
2285
2286 Node::BinaryOp { op, left, right } => {
2287 let lt = self.infer_type(left, scope);
2288 let rt = self.infer_type(right, scope);
2289 infer_binary_op_type(op, <, &rt)
2290 }
2291
2292 Node::UnaryOp { op, operand } => {
2293 let t = self.infer_type(operand, scope);
2294 match op.as_str() {
2295 "!" => Some(TypeExpr::Named("bool".into())),
2296 "-" => t, _ => None,
2298 }
2299 }
2300
2301 Node::Ternary {
2302 condition,
2303 true_expr,
2304 false_expr,
2305 } => {
2306 let refs = Self::extract_refinements(condition, scope);
2307
2308 let mut true_scope = scope.child();
2309 apply_refinements(&mut true_scope, &refs.truthy);
2310 let tt = self.infer_type(true_expr, &true_scope);
2311
2312 let mut false_scope = scope.child();
2313 apply_refinements(&mut false_scope, &refs.falsy);
2314 let ft = self.infer_type(false_expr, &false_scope);
2315
2316 match (&tt, &ft) {
2317 (Some(a), Some(b)) if a == b => tt,
2318 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
2319 (Some(_), None) => tt,
2320 (None, Some(_)) => ft,
2321 (None, None) => None,
2322 }
2323 }
2324
2325 Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
2326
2327 Node::PropertyAccess { object, property } => {
2328 if let Node::Identifier(name) = &object.node {
2330 if scope.get_enum(name).is_some() {
2331 return Some(TypeExpr::Named(name.clone()));
2332 }
2333 }
2334 if property == "variant" {
2336 let obj_type = self.infer_type(object, scope);
2337 if let Some(TypeExpr::Named(name)) = &obj_type {
2338 if scope.get_enum(name).is_some() {
2339 return Some(TypeExpr::Named("string".into()));
2340 }
2341 }
2342 }
2343 let obj_type = self.infer_type(object, scope);
2345 if let Some(TypeExpr::Shape(fields)) = &obj_type {
2346 if let Some(field) = fields.iter().find(|f| f.name == *property) {
2347 return Some(field.type_expr.clone());
2348 }
2349 }
2350 None
2351 }
2352
2353 Node::SubscriptAccess { object, index } => {
2354 let obj_type = self.infer_type(object, scope);
2355 match &obj_type {
2356 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
2357 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
2358 Some(TypeExpr::Shape(fields)) => {
2359 if let Node::StringLiteral(key) = &index.node {
2361 fields
2362 .iter()
2363 .find(|f| &f.name == key)
2364 .map(|f| f.type_expr.clone())
2365 } else {
2366 None
2367 }
2368 }
2369 Some(TypeExpr::Named(n)) if n == "list" => None,
2370 Some(TypeExpr::Named(n)) if n == "dict" => None,
2371 Some(TypeExpr::Named(n)) if n == "string" => {
2372 Some(TypeExpr::Named("string".into()))
2373 }
2374 _ => None,
2375 }
2376 }
2377 Node::SliceAccess { object, .. } => {
2378 let obj_type = self.infer_type(object, scope);
2380 match &obj_type {
2381 Some(TypeExpr::List(_)) => obj_type,
2382 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
2383 Some(TypeExpr::Named(n)) if n == "string" => {
2384 Some(TypeExpr::Named("string".into()))
2385 }
2386 _ => None,
2387 }
2388 }
2389 Node::MethodCall { object, method, .. }
2390 | Node::OptionalMethodCall { object, method, .. } => {
2391 let obj_type = self.infer_type(object, scope);
2392 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
2393 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
2394 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
2395 match method.as_str() {
2396 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
2398 Some(TypeExpr::Named("bool".into()))
2399 }
2400 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
2402 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
2404 | "pad_left" | "pad_right" | "repeat" | "join" => {
2405 Some(TypeExpr::Named("string".into()))
2406 }
2407 "split" | "chars" => Some(TypeExpr::Named("list".into())),
2408 "filter" => {
2410 if is_dict {
2411 Some(TypeExpr::Named("dict".into()))
2412 } else {
2413 Some(TypeExpr::Named("list".into()))
2414 }
2415 }
2416 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2418 "reduce" | "find" | "first" | "last" => None,
2419 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2421 "merge" | "map_values" | "rekey" | "map_keys" => {
2422 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2426 Some(TypeExpr::DictType(
2427 Box::new(TypeExpr::Named("string".into())),
2428 v.clone(),
2429 ))
2430 } else {
2431 Some(TypeExpr::Named("dict".into()))
2432 }
2433 }
2434 "to_string" => Some(TypeExpr::Named("string".into())),
2436 "to_int" => Some(TypeExpr::Named("int".into())),
2437 "to_float" => Some(TypeExpr::Named("float".into())),
2438 _ => None,
2439 }
2440 }
2441
2442 Node::TryOperator { operand } => {
2444 match self.infer_type(operand, scope) {
2445 Some(TypeExpr::Named(name)) if name == "Result" => None, _ => None,
2447 }
2448 }
2449
2450 Node::ThrowStmt { .. }
2452 | Node::ReturnStmt { .. }
2453 | Node::BreakStmt
2454 | Node::ContinueStmt => Some(TypeExpr::Never),
2455
2456 Node::IfElse {
2458 then_body,
2459 else_body,
2460 ..
2461 } => {
2462 let then_type = self.infer_block_type(then_body, scope);
2463 let else_type = else_body
2464 .as_ref()
2465 .and_then(|eb| self.infer_block_type(eb, scope));
2466 match (then_type, else_type) {
2467 (Some(TypeExpr::Never), Some(TypeExpr::Never)) => Some(TypeExpr::Never),
2468 (Some(TypeExpr::Never), Some(other)) | (Some(other), Some(TypeExpr::Never)) => {
2469 Some(other)
2470 }
2471 (Some(t), Some(e)) if t == e => Some(t),
2472 (Some(t), Some(e)) => Some(simplify_union(vec![t, e])),
2473 (Some(t), None) => Some(t),
2474 (None, _) => None,
2475 }
2476 }
2477
2478 _ => None,
2479 }
2480 }
2481
2482 fn infer_block_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
2484 if Self::block_definitely_exits(stmts) {
2485 return Some(TypeExpr::Never);
2486 }
2487 stmts.last().and_then(|s| self.infer_type(s, scope))
2488 }
2489
2490 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2492 if let TypeExpr::Named(name) = expected {
2494 if scope.is_generic_type_param(name) {
2495 return true;
2496 }
2497 }
2498 if let TypeExpr::Named(name) = actual {
2499 if scope.is_generic_type_param(name) {
2500 return true;
2501 }
2502 }
2503 let expected = self.resolve_alias(expected, scope);
2504 let actual = self.resolve_alias(actual, scope);
2505
2506 if let TypeExpr::Named(iface_name) = &expected {
2509 if scope.get_interface(iface_name).is_some() {
2510 if let TypeExpr::Named(type_name) = &actual {
2511 return self.satisfies_interface(type_name, iface_name, scope);
2512 }
2513 return false;
2514 }
2515 }
2516
2517 match (&expected, &actual) {
2518 (_, TypeExpr::Never) => true,
2520 (TypeExpr::Never, _) => false,
2522 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2523 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2526 act_members.iter().all(|am| {
2527 exp_members
2528 .iter()
2529 .any(|em| self.types_compatible(em, am, scope))
2530 })
2531 }
2532 (TypeExpr::Union(members), actual_type) => members
2533 .iter()
2534 .any(|m| self.types_compatible(m, actual_type, scope)),
2535 (expected_type, TypeExpr::Union(members)) => members
2536 .iter()
2537 .all(|m| self.types_compatible(expected_type, m, scope)),
2538 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2539 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2540 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2541 if expected_field.optional {
2542 return true;
2543 }
2544 af.iter().any(|actual_field| {
2545 actual_field.name == expected_field.name
2546 && self.types_compatible(
2547 &expected_field.type_expr,
2548 &actual_field.type_expr,
2549 scope,
2550 )
2551 })
2552 }),
2553 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2555 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2556 keys_ok
2557 && af
2558 .iter()
2559 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2560 }
2561 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2563 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2564 self.types_compatible(expected_inner, actual_inner, scope)
2565 }
2566 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2567 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2568 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2569 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2570 }
2571 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2572 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2573 (
2575 TypeExpr::FnType {
2576 params: ep,
2577 return_type: er,
2578 },
2579 TypeExpr::FnType {
2580 params: ap,
2581 return_type: ar,
2582 },
2583 ) => {
2584 ep.len() == ap.len()
2585 && ep
2586 .iter()
2587 .zip(ap.iter())
2588 .all(|(e, a)| self.types_compatible(e, a, scope))
2589 && self.types_compatible(er, ar, scope)
2590 }
2591 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2593 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2594 _ => false,
2595 }
2596 }
2597
2598 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2599 if let TypeExpr::Named(name) = ty {
2600 if let Some(resolved) = scope.resolve_type(name) {
2601 return resolved.clone();
2602 }
2603 }
2604 ty.clone()
2605 }
2606
2607 fn error_at(&mut self, message: String, span: Span) {
2608 self.diagnostics.push(TypeDiagnostic {
2609 message,
2610 severity: DiagnosticSeverity::Error,
2611 span: Some(span),
2612 help: None,
2613 fix: None,
2614 });
2615 }
2616
2617 #[allow(dead_code)]
2618 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2619 self.diagnostics.push(TypeDiagnostic {
2620 message,
2621 severity: DiagnosticSeverity::Error,
2622 span: Some(span),
2623 help: Some(help),
2624 fix: None,
2625 });
2626 }
2627
2628 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
2629 self.diagnostics.push(TypeDiagnostic {
2630 message,
2631 severity: DiagnosticSeverity::Error,
2632 span: Some(span),
2633 help: None,
2634 fix: Some(fix),
2635 });
2636 }
2637
2638 fn warning_at(&mut self, message: String, span: Span) {
2639 self.diagnostics.push(TypeDiagnostic {
2640 message,
2641 severity: DiagnosticSeverity::Warning,
2642 span: Some(span),
2643 help: None,
2644 fix: None,
2645 });
2646 }
2647
2648 #[allow(dead_code)]
2649 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2650 self.diagnostics.push(TypeDiagnostic {
2651 message,
2652 severity: DiagnosticSeverity::Warning,
2653 span: Some(span),
2654 help: Some(help),
2655 fix: None,
2656 });
2657 }
2658
2659 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
2663 match &snode.node {
2664 Node::BinaryOp { op, left, right } => {
2665 self.check_binops(left, scope);
2666 self.check_binops(right, scope);
2667 let lt = self.infer_type(left, scope);
2668 let rt = self.infer_type(right, scope);
2669 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
2670 let span = snode.span;
2671 match op.as_str() {
2672 "+" => {
2673 let valid = matches!(
2674 (l.as_str(), r.as_str()),
2675 ("int" | "float", "int" | "float")
2676 | ("string", "string")
2677 | ("list", "list")
2678 | ("dict", "dict")
2679 );
2680 if !valid {
2681 let msg =
2682 format!("Operator '+' is not valid for types {} and {}", l, r);
2683 let fix = if l == "string" || r == "string" {
2684 self.build_interpolation_fix(left, right, l == "string", span)
2685 } else {
2686 None
2687 };
2688 if let Some(fix) = fix {
2689 self.error_at_with_fix(msg, span, fix);
2690 } else {
2691 self.error_at(msg, span);
2692 }
2693 }
2694 }
2695 "-" | "/" | "%" => {
2696 let numeric = ["int", "float"];
2697 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
2698 self.error_at(
2699 format!(
2700 "Operator '{}' requires numeric operands, got {} and {}",
2701 op, l, r
2702 ),
2703 span,
2704 );
2705 }
2706 }
2707 "*" => {
2708 let numeric = ["int", "float"];
2709 let is_numeric =
2710 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
2711 let is_string_repeat =
2712 (l == "string" && r == "int") || (l == "int" && r == "string");
2713 if !is_numeric && !is_string_repeat {
2714 self.error_at(
2715 format!(
2716 "Operator '*' requires numeric operands or string * int, got {} and {}",
2717 l, r
2718 ),
2719 span,
2720 );
2721 }
2722 }
2723 _ => {}
2724 }
2725 }
2726 }
2727 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
2729 _ => {}
2730 }
2731 }
2732
2733 fn build_interpolation_fix(
2735 &self,
2736 left: &SNode,
2737 right: &SNode,
2738 left_is_string: bool,
2739 expr_span: Span,
2740 ) -> Option<Vec<FixEdit>> {
2741 let src = self.source.as_ref()?;
2742 let (str_node, other_node) = if left_is_string {
2743 (left, right)
2744 } else {
2745 (right, left)
2746 };
2747 let str_text = src.get(str_node.span.start..str_node.span.end)?;
2748 let other_text = src.get(other_node.span.start..other_node.span.end)?;
2749 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
2751 if other_text.contains('}') || other_text.contains('"') {
2753 return None;
2754 }
2755 let replacement = if left_is_string {
2756 format!("\"{inner}${{{other_text}}}\"")
2757 } else {
2758 format!("\"${{{other_text}}}{inner}\"")
2759 };
2760 Some(vec![FixEdit {
2761 span: expr_span,
2762 replacement,
2763 }])
2764 }
2765}
2766
2767impl Default for TypeChecker {
2768 fn default() -> Self {
2769 Self::new()
2770 }
2771}
2772
2773fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2775 match op {
2776 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2777 Some(TypeExpr::Named("bool".into()))
2778 }
2779 "+" => match (left, right) {
2780 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2781 match (l.as_str(), r.as_str()) {
2782 ("int", "int") => Some(TypeExpr::Named("int".into())),
2783 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2784 ("string", "string") => Some(TypeExpr::Named("string".into())),
2785 ("list", "list") => Some(TypeExpr::Named("list".into())),
2786 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2787 _ => None,
2788 }
2789 }
2790 _ => None,
2791 },
2792 "-" | "/" | "%" => match (left, right) {
2793 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2794 match (l.as_str(), r.as_str()) {
2795 ("int", "int") => Some(TypeExpr::Named("int".into())),
2796 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2797 _ => None,
2798 }
2799 }
2800 _ => None,
2801 },
2802 "*" => match (left, right) {
2803 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2804 match (l.as_str(), r.as_str()) {
2805 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2806 ("int", "int") => Some(TypeExpr::Named("int".into())),
2807 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2808 _ => None,
2809 }
2810 }
2811 _ => None,
2812 },
2813 "??" => match (left, right) {
2814 (Some(TypeExpr::Union(members)), _) => {
2816 let non_nil: Vec<_> = members
2817 .iter()
2818 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2819 .cloned()
2820 .collect();
2821 if non_nil.len() == 1 {
2822 Some(non_nil[0].clone())
2823 } else if non_nil.is_empty() {
2824 right.clone()
2825 } else {
2826 Some(TypeExpr::Union(non_nil))
2827 }
2828 }
2829 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
2831 (Some(l), _) => Some(l.clone()),
2833 (None, _) => right.clone(),
2835 },
2836 "|>" => None,
2837 _ => None,
2838 }
2839}
2840
2841pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2846 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2847 let mut details = Vec::new();
2848 for field in ef {
2849 if field.optional {
2850 continue;
2851 }
2852 match af.iter().find(|f| f.name == field.name) {
2853 None => details.push(format!(
2854 "missing field '{}' ({})",
2855 field.name,
2856 format_type(&field.type_expr)
2857 )),
2858 Some(actual_field) => {
2859 let e_str = format_type(&field.type_expr);
2860 let a_str = format_type(&actual_field.type_expr);
2861 if e_str != a_str {
2862 details.push(format!(
2863 "field '{}' has type {}, expected {}",
2864 field.name, a_str, e_str
2865 ));
2866 }
2867 }
2868 }
2869 }
2870 if details.is_empty() {
2871 None
2872 } else {
2873 Some(details.join("; "))
2874 }
2875 } else {
2876 None
2877 }
2878}
2879
2880fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
2883 matches!(
2884 &value.node,
2885 Node::IntLiteral(_)
2886 | Node::FloatLiteral(_)
2887 | Node::StringLiteral(_)
2888 | Node::BoolLiteral(_)
2889 | Node::NilLiteral
2890 | Node::ListLiteral(_)
2891 | Node::DictLiteral(_)
2892 | Node::InterpolatedString(_)
2893 )
2894}
2895
2896pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
2899 match &stmt.node {
2900 Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
2901 true
2902 }
2903 Node::IfElse {
2904 then_body,
2905 else_body: Some(else_body),
2906 ..
2907 } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
2908 _ => false,
2909 }
2910}
2911
2912pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
2914 stmts.iter().any(stmt_definitely_exits)
2915}
2916
2917pub fn format_type(ty: &TypeExpr) -> String {
2918 match ty {
2919 TypeExpr::Named(n) => n.clone(),
2920 TypeExpr::Union(types) => types
2921 .iter()
2922 .map(format_type)
2923 .collect::<Vec<_>>()
2924 .join(" | "),
2925 TypeExpr::Shape(fields) => {
2926 let inner: Vec<String> = fields
2927 .iter()
2928 .map(|f| {
2929 let opt = if f.optional { "?" } else { "" };
2930 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2931 })
2932 .collect();
2933 format!("{{{}}}", inner.join(", "))
2934 }
2935 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2936 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2937 TypeExpr::FnType {
2938 params,
2939 return_type,
2940 } => {
2941 let params_str = params
2942 .iter()
2943 .map(format_type)
2944 .collect::<Vec<_>>()
2945 .join(", ");
2946 format!("fn({}) -> {}", params_str, format_type(return_type))
2947 }
2948 TypeExpr::Never => "never".to_string(),
2949 }
2950}
2951
2952fn simplify_union(members: Vec<TypeExpr>) -> TypeExpr {
2954 let filtered: Vec<TypeExpr> = members
2955 .into_iter()
2956 .filter(|m| !matches!(m, TypeExpr::Never))
2957 .collect();
2958 match filtered.len() {
2959 0 => TypeExpr::Never,
2960 1 => filtered.into_iter().next().unwrap(),
2961 _ => TypeExpr::Union(filtered),
2962 }
2963}
2964
2965fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2968 let remaining: Vec<TypeExpr> = members
2969 .iter()
2970 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2971 .cloned()
2972 .collect();
2973 match remaining.len() {
2974 0 => Some(TypeExpr::Never),
2975 1 => Some(remaining.into_iter().next().unwrap()),
2976 _ => Some(TypeExpr::Union(remaining)),
2977 }
2978}
2979
2980fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2982 if members
2983 .iter()
2984 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2985 {
2986 Some(TypeExpr::Named(target.to_string()))
2987 } else {
2988 None
2989 }
2990}
2991
2992fn extract_type_of_var(node: &SNode) -> Option<String> {
2994 if let Node::FunctionCall { name, args } = &node.node {
2995 if name == "type_of" && args.len() == 1 {
2996 if let Node::Identifier(var) = &args[0].node {
2997 return Some(var.clone());
2998 }
2999 }
3000 }
3001 None
3002}
3003
3004fn schema_type_expr_from_node(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3005 match &node.node {
3006 Node::Identifier(name) => scope.get_schema_binding(name).cloned().flatten(),
3007 Node::DictLiteral(entries) => schema_type_expr_from_dict(entries, scope),
3008 _ => None,
3009 }
3010}
3011
3012fn schema_type_expr_from_dict(entries: &[DictEntry], scope: &TypeScope) -> Option<TypeExpr> {
3013 let mut type_name: Option<String> = None;
3014 let mut properties: Option<&SNode> = None;
3015 let mut required: Option<Vec<String>> = None;
3016 let mut items: Option<&SNode> = None;
3017 let mut union: Option<&SNode> = None;
3018 let mut nullable = false;
3019 let mut additional_properties: Option<&SNode> = None;
3020
3021 for entry in entries {
3022 let key = schema_entry_key(&entry.key)?;
3023 match key.as_str() {
3024 "type" => match &entry.value.node {
3025 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3026 type_name = Some(normalize_schema_type_name(text));
3027 }
3028 Node::ListLiteral(items_list) => {
3029 let union_members = items_list
3030 .iter()
3031 .filter_map(|item| match &item.node {
3032 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3033 Some(TypeExpr::Named(normalize_schema_type_name(text)))
3034 }
3035 _ => None,
3036 })
3037 .collect::<Vec<_>>();
3038 if !union_members.is_empty() {
3039 return Some(TypeExpr::Union(union_members));
3040 }
3041 }
3042 _ => {}
3043 },
3044 "properties" => properties = Some(&entry.value),
3045 "required" => {
3046 required = schema_required_names(&entry.value);
3047 }
3048 "items" => items = Some(&entry.value),
3049 "union" | "oneOf" | "anyOf" => union = Some(&entry.value),
3050 "nullable" => {
3051 nullable = matches!(entry.value.node, Node::BoolLiteral(true));
3052 }
3053 "additional_properties" | "additionalProperties" => {
3054 additional_properties = Some(&entry.value);
3055 }
3056 _ => {}
3057 }
3058 }
3059
3060 let mut schema_type = if let Some(union_node) = union {
3061 schema_union_type_expr(union_node, scope)?
3062 } else if let Some(properties_node) = properties {
3063 let property_entries = match &properties_node.node {
3064 Node::DictLiteral(entries) => entries,
3065 _ => return None,
3066 };
3067 let required_names = required.unwrap_or_default();
3068 let mut fields = Vec::new();
3069 for entry in property_entries {
3070 let field_name = schema_entry_key(&entry.key)?;
3071 let field_type = schema_type_expr_from_node(&entry.value, scope)?;
3072 fields.push(ShapeField {
3073 name: field_name.clone(),
3074 type_expr: field_type,
3075 optional: !required_names.contains(&field_name),
3076 });
3077 }
3078 TypeExpr::Shape(fields)
3079 } else if let Some(item_node) = items {
3080 TypeExpr::List(Box::new(schema_type_expr_from_node(item_node, scope)?))
3081 } else if let Some(type_name) = type_name {
3082 if type_name == "dict" {
3083 if let Some(extra_node) = additional_properties {
3084 let value_type = match &extra_node.node {
3085 Node::BoolLiteral(_) => None,
3086 _ => schema_type_expr_from_node(extra_node, scope),
3087 };
3088 if let Some(value_type) = value_type {
3089 TypeExpr::DictType(
3090 Box::new(TypeExpr::Named("string".into())),
3091 Box::new(value_type),
3092 )
3093 } else {
3094 TypeExpr::Named(type_name)
3095 }
3096 } else {
3097 TypeExpr::Named(type_name)
3098 }
3099 } else {
3100 TypeExpr::Named(type_name)
3101 }
3102 } else {
3103 return None;
3104 };
3105
3106 if nullable {
3107 schema_type = match schema_type {
3108 TypeExpr::Union(mut members) => {
3109 if !members
3110 .iter()
3111 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil"))
3112 {
3113 members.push(TypeExpr::Named("nil".into()));
3114 }
3115 TypeExpr::Union(members)
3116 }
3117 other => TypeExpr::Union(vec![other, TypeExpr::Named("nil".into())]),
3118 };
3119 }
3120
3121 Some(schema_type)
3122}
3123
3124fn schema_union_type_expr(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3125 let Node::ListLiteral(items) = &node.node else {
3126 return None;
3127 };
3128 let members = items
3129 .iter()
3130 .filter_map(|item| schema_type_expr_from_node(item, scope))
3131 .collect::<Vec<_>>();
3132 match members.len() {
3133 0 => None,
3134 1 => members.into_iter().next(),
3135 _ => Some(TypeExpr::Union(members)),
3136 }
3137}
3138
3139fn schema_required_names(node: &SNode) -> Option<Vec<String>> {
3140 let Node::ListLiteral(items) = &node.node else {
3141 return None;
3142 };
3143 Some(
3144 items
3145 .iter()
3146 .filter_map(|item| match &item.node {
3147 Node::StringLiteral(text) | Node::RawStringLiteral(text) => Some(text.clone()),
3148 Node::Identifier(text) => Some(text.clone()),
3149 _ => None,
3150 })
3151 .collect(),
3152 )
3153}
3154
3155fn schema_entry_key(node: &SNode) -> Option<String> {
3156 match &node.node {
3157 Node::Identifier(name) => Some(name.clone()),
3158 Node::StringLiteral(name) | Node::RawStringLiteral(name) => Some(name.clone()),
3159 _ => None,
3160 }
3161}
3162
3163fn normalize_schema_type_name(text: &str) -> String {
3164 match text {
3165 "object" => "dict".into(),
3166 "array" => "list".into(),
3167 "integer" => "int".into(),
3168 "number" => "float".into(),
3169 "boolean" => "bool".into(),
3170 "null" => "nil".into(),
3171 other => other.into(),
3172 }
3173}
3174
3175fn intersect_types(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
3176 match (current, schema_type) {
3177 (TypeExpr::Union(members), other) => {
3178 let kept = members
3179 .iter()
3180 .filter_map(|member| intersect_types(member, other))
3181 .collect::<Vec<_>>();
3182 match kept.len() {
3183 0 => None,
3184 1 => kept.into_iter().next(),
3185 _ => Some(TypeExpr::Union(kept)),
3186 }
3187 }
3188 (other, TypeExpr::Union(members)) => {
3189 let kept = members
3190 .iter()
3191 .filter_map(|member| intersect_types(other, member))
3192 .collect::<Vec<_>>();
3193 match kept.len() {
3194 0 => None,
3195 1 => kept.into_iter().next(),
3196 _ => Some(TypeExpr::Union(kept)),
3197 }
3198 }
3199 (TypeExpr::Named(left), TypeExpr::Named(right)) if left == right => {
3200 Some(TypeExpr::Named(left.clone()))
3201 }
3202 (TypeExpr::Named(name), TypeExpr::Shape(fields)) if name == "dict" => {
3203 Some(TypeExpr::Shape(fields.clone()))
3204 }
3205 (TypeExpr::Shape(fields), TypeExpr::Named(name)) if name == "dict" => {
3206 Some(TypeExpr::Shape(fields.clone()))
3207 }
3208 (TypeExpr::Named(name), TypeExpr::List(inner)) if name == "list" => {
3209 Some(TypeExpr::List(inner.clone()))
3210 }
3211 (TypeExpr::List(inner), TypeExpr::Named(name)) if name == "list" => {
3212 Some(TypeExpr::List(inner.clone()))
3213 }
3214 (TypeExpr::Named(name), TypeExpr::DictType(key, value)) if name == "dict" => {
3215 Some(TypeExpr::DictType(key.clone(), value.clone()))
3216 }
3217 (TypeExpr::DictType(key, value), TypeExpr::Named(name)) if name == "dict" => {
3218 Some(TypeExpr::DictType(key.clone(), value.clone()))
3219 }
3220 (TypeExpr::Shape(_), TypeExpr::Shape(fields)) => Some(TypeExpr::Shape(fields.clone())),
3221 (TypeExpr::List(current_inner), TypeExpr::List(schema_inner)) => {
3222 intersect_types(current_inner, schema_inner)
3223 .map(|inner| TypeExpr::List(Box::new(inner)))
3224 }
3225 (
3226 TypeExpr::DictType(current_key, current_value),
3227 TypeExpr::DictType(schema_key, schema_value),
3228 ) => {
3229 let key = intersect_types(current_key, schema_key)?;
3230 let value = intersect_types(current_value, schema_value)?;
3231 Some(TypeExpr::DictType(Box::new(key), Box::new(value)))
3232 }
3233 _ => None,
3234 }
3235}
3236
3237fn subtract_type(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
3238 match current {
3239 TypeExpr::Union(members) => {
3240 let remaining = members
3241 .iter()
3242 .filter(|member| intersect_types(member, schema_type).is_none())
3243 .cloned()
3244 .collect::<Vec<_>>();
3245 match remaining.len() {
3246 0 => None,
3247 1 => remaining.into_iter().next(),
3248 _ => Some(TypeExpr::Union(remaining)),
3249 }
3250 }
3251 other if intersect_types(other, schema_type).is_some() => None,
3252 other => Some(other.clone()),
3253 }
3254}
3255
3256fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
3258 for (var_name, narrowed_type) in refinements {
3259 if !scope.narrowed_vars.contains_key(var_name) {
3261 if let Some(original) = scope.get_var(var_name).cloned() {
3262 scope.narrowed_vars.insert(var_name.clone(), original);
3263 }
3264 }
3265 scope.define_var(var_name, narrowed_type.clone());
3266 }
3267}
3268
3269#[cfg(test)]
3270mod tests {
3271 use super::*;
3272 use crate::Parser;
3273 use harn_lexer::Lexer;
3274
3275 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
3276 let mut lexer = Lexer::new(source);
3277 let tokens = lexer.tokenize().unwrap();
3278 let mut parser = Parser::new(tokens);
3279 let program = parser.parse().unwrap();
3280 TypeChecker::new().check(&program)
3281 }
3282
3283 fn errors(source: &str) -> Vec<String> {
3284 check_source(source)
3285 .into_iter()
3286 .filter(|d| d.severity == DiagnosticSeverity::Error)
3287 .map(|d| d.message)
3288 .collect()
3289 }
3290
3291 #[test]
3292 fn test_no_errors_for_untyped_code() {
3293 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
3294 assert!(errs.is_empty());
3295 }
3296
3297 #[test]
3298 fn test_correct_typed_let() {
3299 let errs = errors("pipeline t(task) { let x: int = 42 }");
3300 assert!(errs.is_empty());
3301 }
3302
3303 #[test]
3304 fn test_type_mismatch_let() {
3305 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
3306 assert_eq!(errs.len(), 1);
3307 assert!(errs[0].contains("Type mismatch"));
3308 assert!(errs[0].contains("int"));
3309 assert!(errs[0].contains("string"));
3310 }
3311
3312 #[test]
3313 fn test_correct_typed_fn() {
3314 let errs = errors(
3315 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
3316 );
3317 assert!(errs.is_empty());
3318 }
3319
3320 #[test]
3321 fn test_fn_arg_type_mismatch() {
3322 let errs = errors(
3323 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
3324add("hello", 2) }"#,
3325 );
3326 assert_eq!(errs.len(), 1);
3327 assert!(errs[0].contains("Argument 1"));
3328 assert!(errs[0].contains("expected int"));
3329 }
3330
3331 #[test]
3332 fn test_return_type_mismatch() {
3333 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
3334 assert_eq!(errs.len(), 1);
3335 assert!(errs[0].contains("Return type mismatch"));
3336 }
3337
3338 #[test]
3339 fn test_union_type_compatible() {
3340 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
3341 assert!(errs.is_empty());
3342 }
3343
3344 #[test]
3345 fn test_union_type_mismatch() {
3346 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
3347 assert_eq!(errs.len(), 1);
3348 assert!(errs[0].contains("Type mismatch"));
3349 }
3350
3351 #[test]
3352 fn test_type_inference_propagation() {
3353 let errs = errors(
3354 r#"pipeline t(task) {
3355 fn add(a: int, b: int) -> int { return a + b }
3356 let result: string = add(1, 2)
3357}"#,
3358 );
3359 assert_eq!(errs.len(), 1);
3360 assert!(errs[0].contains("Type mismatch"));
3361 assert!(errs[0].contains("string"));
3362 assert!(errs[0].contains("int"));
3363 }
3364
3365 #[test]
3366 fn test_generic_return_type_instantiates_from_callsite() {
3367 let errs = errors(
3368 r#"pipeline t(task) {
3369 fn identity<T>(x: T) -> T { return x }
3370 fn first<T>(items: list<T>) -> T { return items[0] }
3371 let n: int = identity(42)
3372 let s: string = first(["a", "b"])
3373}"#,
3374 );
3375 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
3376 }
3377
3378 #[test]
3379 fn test_generic_type_param_must_bind_consistently() {
3380 let errs = errors(
3381 r#"pipeline t(task) {
3382 fn keep<T>(a: T, b: T) -> T { return a }
3383 keep(1, "x")
3384}"#,
3385 );
3386 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
3387 assert!(
3388 errs.iter()
3389 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
3390 "missing generic binding conflict error: {:?}",
3391 errs
3392 );
3393 assert!(
3394 errs.iter()
3395 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
3396 "missing instantiated argument mismatch error: {:?}",
3397 errs
3398 );
3399 }
3400
3401 #[test]
3402 fn test_generic_list_binding_propagates_element_type() {
3403 let errs = errors(
3404 r#"pipeline t(task) {
3405 fn first<T>(items: list<T>) -> T { return items[0] }
3406 let bad: string = first([1, 2, 3])
3407}"#,
3408 );
3409 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3410 assert!(errs[0].contains("declared as string, but assigned int"));
3411 }
3412
3413 #[test]
3414 fn test_builtin_return_type_inference() {
3415 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
3416 assert_eq!(errs.len(), 1);
3417 assert!(errs[0].contains("string"));
3418 assert!(errs[0].contains("int"));
3419 }
3420
3421 #[test]
3422 fn test_workflow_and_transcript_builtins_are_known() {
3423 let errs = errors(
3424 r#"pipeline t(task) {
3425 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
3426 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
3427 let run: dict = workflow_execute("task", flow, [], {})
3428 let tree: dict = load_run_tree("run.json")
3429 let fixture: dict = run_record_fixture(run?.run)
3430 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
3431 let diff: dict = run_record_diff(run?.run, run?.run)
3432 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
3433 let suite_report: dict = eval_suite_run(manifest)
3434 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
3435 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
3436 let selection: dict = artifact_editor_selection("src/main.rs", "main")
3437 let verify: dict = artifact_verification_result("verify", "ok")
3438 let test_result: dict = artifact_test_result("tests", "pass")
3439 let cmd: dict = artifact_command_result("cargo test", {status: 0})
3440 let patch: dict = artifact_diff("src/main.rs", "old", "new")
3441 let git: dict = artifact_git_diff("diff --git a b")
3442 let review: dict = artifact_diff_review(patch, "review me")
3443 let decision: dict = artifact_review_decision(review, "accepted")
3444 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
3445 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
3446 let apply: dict = artifact_apply_intent(review, "apply")
3447 let transcript = transcript_reset({metadata: {source: "test"}})
3448 let visible: string = transcript_render_visible(transcript_archive(transcript))
3449 let events: list = transcript_events(transcript)
3450 let context: string = artifact_context([], {max_artifacts: 1})
3451 println(report)
3452 println(run)
3453 println(tree)
3454 println(fixture)
3455 println(suite)
3456 println(diff)
3457 println(manifest)
3458 println(suite_report)
3459 println(wf)
3460 println(snap)
3461 println(selection)
3462 println(verify)
3463 println(test_result)
3464 println(cmd)
3465 println(patch)
3466 println(git)
3467 println(review)
3468 println(decision)
3469 println(proposal)
3470 println(bundle)
3471 println(apply)
3472 println(visible)
3473 println(events)
3474 println(context)
3475}"#,
3476 );
3477 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
3478 }
3479
3480 #[test]
3481 fn test_binary_op_type_inference() {
3482 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
3483 assert_eq!(errs.len(), 1);
3484 }
3485
3486 #[test]
3487 fn test_comparison_returns_bool() {
3488 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
3489 assert!(errs.is_empty());
3490 }
3491
3492 #[test]
3493 fn test_int_float_promotion() {
3494 let errs = errors("pipeline t(task) { let x: float = 42 }");
3495 assert!(errs.is_empty());
3496 }
3497
3498 #[test]
3499 fn test_untyped_code_no_errors() {
3500 let errs = errors(
3501 r#"pipeline t(task) {
3502 fn process(data) {
3503 let result = data + " processed"
3504 return result
3505 }
3506 log(process("hello"))
3507}"#,
3508 );
3509 assert!(errs.is_empty());
3510 }
3511
3512 #[test]
3513 fn test_type_alias() {
3514 let errs = errors(
3515 r#"pipeline t(task) {
3516 type Name = string
3517 let x: Name = "hello"
3518}"#,
3519 );
3520 assert!(errs.is_empty());
3521 }
3522
3523 #[test]
3524 fn test_type_alias_mismatch() {
3525 let errs = errors(
3526 r#"pipeline t(task) {
3527 type Name = string
3528 let x: Name = 42
3529}"#,
3530 );
3531 assert_eq!(errs.len(), 1);
3532 }
3533
3534 #[test]
3535 fn test_assignment_type_check() {
3536 let errs = errors(
3537 r#"pipeline t(task) {
3538 var x: int = 0
3539 x = "hello"
3540}"#,
3541 );
3542 assert_eq!(errs.len(), 1);
3543 assert!(errs[0].contains("cannot assign string"));
3544 }
3545
3546 #[test]
3547 fn test_covariance_int_to_float_in_fn() {
3548 let errs = errors(
3549 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
3550 );
3551 assert!(errs.is_empty());
3552 }
3553
3554 #[test]
3555 fn test_covariance_return_type() {
3556 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
3557 assert!(errs.is_empty());
3558 }
3559
3560 #[test]
3561 fn test_no_contravariance_float_to_int() {
3562 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
3563 assert_eq!(errs.len(), 1);
3564 }
3565
3566 fn warnings(source: &str) -> Vec<String> {
3569 check_source(source)
3570 .into_iter()
3571 .filter(|d| d.severity == DiagnosticSeverity::Warning)
3572 .map(|d| d.message)
3573 .collect()
3574 }
3575
3576 #[test]
3577 fn test_exhaustive_match_no_warning() {
3578 let warns = warnings(
3579 r#"pipeline t(task) {
3580 enum Color { Red, Green, Blue }
3581 let c = Color.Red
3582 match c.variant {
3583 "Red" -> { log("r") }
3584 "Green" -> { log("g") }
3585 "Blue" -> { log("b") }
3586 }
3587}"#,
3588 );
3589 let exhaustive_warns: Vec<_> = warns
3590 .iter()
3591 .filter(|w| w.contains("Non-exhaustive"))
3592 .collect();
3593 assert!(exhaustive_warns.is_empty());
3594 }
3595
3596 #[test]
3597 fn test_non_exhaustive_match_warning() {
3598 let warns = warnings(
3599 r#"pipeline t(task) {
3600 enum Color { Red, Green, Blue }
3601 let c = Color.Red
3602 match c.variant {
3603 "Red" -> { log("r") }
3604 "Green" -> { log("g") }
3605 }
3606}"#,
3607 );
3608 let exhaustive_warns: Vec<_> = warns
3609 .iter()
3610 .filter(|w| w.contains("Non-exhaustive"))
3611 .collect();
3612 assert_eq!(exhaustive_warns.len(), 1);
3613 assert!(exhaustive_warns[0].contains("Blue"));
3614 }
3615
3616 #[test]
3617 fn test_non_exhaustive_multiple_missing() {
3618 let warns = warnings(
3619 r#"pipeline t(task) {
3620 enum Status { Active, Inactive, Pending }
3621 let s = Status.Active
3622 match s.variant {
3623 "Active" -> { log("a") }
3624 }
3625}"#,
3626 );
3627 let exhaustive_warns: Vec<_> = warns
3628 .iter()
3629 .filter(|w| w.contains("Non-exhaustive"))
3630 .collect();
3631 assert_eq!(exhaustive_warns.len(), 1);
3632 assert!(exhaustive_warns[0].contains("Inactive"));
3633 assert!(exhaustive_warns[0].contains("Pending"));
3634 }
3635
3636 #[test]
3637 fn test_enum_construct_type_inference() {
3638 let errs = errors(
3639 r#"pipeline t(task) {
3640 enum Color { Red, Green, Blue }
3641 let c: Color = Color.Red
3642}"#,
3643 );
3644 assert!(errs.is_empty());
3645 }
3646
3647 #[test]
3650 fn test_nil_coalescing_strips_nil() {
3651 let errs = errors(
3653 r#"pipeline t(task) {
3654 let x: string | nil = nil
3655 let y: string = x ?? "default"
3656}"#,
3657 );
3658 assert!(errs.is_empty());
3659 }
3660
3661 #[test]
3662 fn test_shape_mismatch_detail_missing_field() {
3663 let errs = errors(
3664 r#"pipeline t(task) {
3665 let x: {name: string, age: int} = {name: "hello"}
3666}"#,
3667 );
3668 assert_eq!(errs.len(), 1);
3669 assert!(
3670 errs[0].contains("missing field 'age'"),
3671 "expected detail about missing field, got: {}",
3672 errs[0]
3673 );
3674 }
3675
3676 #[test]
3677 fn test_shape_mismatch_detail_wrong_type() {
3678 let errs = errors(
3679 r#"pipeline t(task) {
3680 let x: {name: string, age: int} = {name: 42, age: 10}
3681}"#,
3682 );
3683 assert_eq!(errs.len(), 1);
3684 assert!(
3685 errs[0].contains("field 'name' has type int, expected string"),
3686 "expected detail about wrong type, got: {}",
3687 errs[0]
3688 );
3689 }
3690
3691 #[test]
3694 fn test_match_pattern_string_against_int() {
3695 let warns = warnings(
3696 r#"pipeline t(task) {
3697 let x: int = 42
3698 match x {
3699 "hello" -> { log("bad") }
3700 42 -> { log("ok") }
3701 }
3702}"#,
3703 );
3704 let pattern_warns: Vec<_> = warns
3705 .iter()
3706 .filter(|w| w.contains("Match pattern type mismatch"))
3707 .collect();
3708 assert_eq!(pattern_warns.len(), 1);
3709 assert!(pattern_warns[0].contains("matching int against string literal"));
3710 }
3711
3712 #[test]
3713 fn test_match_pattern_int_against_string() {
3714 let warns = warnings(
3715 r#"pipeline t(task) {
3716 let x: string = "hello"
3717 match x {
3718 42 -> { log("bad") }
3719 "hello" -> { log("ok") }
3720 }
3721}"#,
3722 );
3723 let pattern_warns: Vec<_> = warns
3724 .iter()
3725 .filter(|w| w.contains("Match pattern type mismatch"))
3726 .collect();
3727 assert_eq!(pattern_warns.len(), 1);
3728 assert!(pattern_warns[0].contains("matching string against int literal"));
3729 }
3730
3731 #[test]
3732 fn test_match_pattern_bool_against_int() {
3733 let warns = warnings(
3734 r#"pipeline t(task) {
3735 let x: int = 42
3736 match x {
3737 true -> { log("bad") }
3738 42 -> { log("ok") }
3739 }
3740}"#,
3741 );
3742 let pattern_warns: Vec<_> = warns
3743 .iter()
3744 .filter(|w| w.contains("Match pattern type mismatch"))
3745 .collect();
3746 assert_eq!(pattern_warns.len(), 1);
3747 assert!(pattern_warns[0].contains("matching int against bool literal"));
3748 }
3749
3750 #[test]
3751 fn test_match_pattern_float_against_string() {
3752 let warns = warnings(
3753 r#"pipeline t(task) {
3754 let x: string = "hello"
3755 match x {
3756 3.14 -> { log("bad") }
3757 "hello" -> { log("ok") }
3758 }
3759}"#,
3760 );
3761 let pattern_warns: Vec<_> = warns
3762 .iter()
3763 .filter(|w| w.contains("Match pattern type mismatch"))
3764 .collect();
3765 assert_eq!(pattern_warns.len(), 1);
3766 assert!(pattern_warns[0].contains("matching string against float literal"));
3767 }
3768
3769 #[test]
3770 fn test_match_pattern_int_against_float_ok() {
3771 let warns = warnings(
3773 r#"pipeline t(task) {
3774 let x: float = 3.14
3775 match x {
3776 42 -> { log("ok") }
3777 _ -> { log("default") }
3778 }
3779}"#,
3780 );
3781 let pattern_warns: Vec<_> = warns
3782 .iter()
3783 .filter(|w| w.contains("Match pattern type mismatch"))
3784 .collect();
3785 assert!(pattern_warns.is_empty());
3786 }
3787
3788 #[test]
3789 fn test_match_pattern_float_against_int_ok() {
3790 let warns = warnings(
3792 r#"pipeline t(task) {
3793 let x: int = 42
3794 match x {
3795 3.14 -> { log("close") }
3796 _ -> { log("default") }
3797 }
3798}"#,
3799 );
3800 let pattern_warns: Vec<_> = warns
3801 .iter()
3802 .filter(|w| w.contains("Match pattern type mismatch"))
3803 .collect();
3804 assert!(pattern_warns.is_empty());
3805 }
3806
3807 #[test]
3808 fn test_match_pattern_correct_types_no_warning() {
3809 let warns = warnings(
3810 r#"pipeline t(task) {
3811 let x: int = 42
3812 match x {
3813 1 -> { log("one") }
3814 2 -> { log("two") }
3815 _ -> { log("other") }
3816 }
3817}"#,
3818 );
3819 let pattern_warns: Vec<_> = warns
3820 .iter()
3821 .filter(|w| w.contains("Match pattern type mismatch"))
3822 .collect();
3823 assert!(pattern_warns.is_empty());
3824 }
3825
3826 #[test]
3827 fn test_match_pattern_wildcard_no_warning() {
3828 let warns = warnings(
3829 r#"pipeline t(task) {
3830 let x: int = 42
3831 match x {
3832 _ -> { log("catch all") }
3833 }
3834}"#,
3835 );
3836 let pattern_warns: Vec<_> = warns
3837 .iter()
3838 .filter(|w| w.contains("Match pattern type mismatch"))
3839 .collect();
3840 assert!(pattern_warns.is_empty());
3841 }
3842
3843 #[test]
3844 fn test_match_pattern_untyped_no_warning() {
3845 let warns = warnings(
3847 r#"pipeline t(task) {
3848 let x = some_unknown_fn()
3849 match x {
3850 "hello" -> { log("string") }
3851 42 -> { log("int") }
3852 }
3853}"#,
3854 );
3855 let pattern_warns: Vec<_> = warns
3856 .iter()
3857 .filter(|w| w.contains("Match pattern type mismatch"))
3858 .collect();
3859 assert!(pattern_warns.is_empty());
3860 }
3861
3862 fn iface_errors(source: &str) -> Vec<String> {
3865 errors(source)
3866 .into_iter()
3867 .filter(|message| message.contains("does not satisfy interface"))
3868 .collect()
3869 }
3870
3871 #[test]
3872 fn test_interface_constraint_return_type_mismatch() {
3873 let warns = iface_errors(
3874 r#"pipeline t(task) {
3875 interface Sizable {
3876 fn size(self) -> int
3877 }
3878 struct Box { width: int }
3879 impl Box {
3880 fn size(self) -> string { return "nope" }
3881 }
3882 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3883 measure(Box({width: 3}))
3884}"#,
3885 );
3886 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3887 assert!(
3888 warns[0].contains("method 'size' returns 'string', expected 'int'"),
3889 "unexpected message: {}",
3890 warns[0]
3891 );
3892 }
3893
3894 #[test]
3895 fn test_interface_constraint_param_type_mismatch() {
3896 let warns = iface_errors(
3897 r#"pipeline t(task) {
3898 interface Processor {
3899 fn process(self, x: int) -> string
3900 }
3901 struct MyProc { name: string }
3902 impl MyProc {
3903 fn process(self, x: string) -> string { return x }
3904 }
3905 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
3906 run_proc(MyProc({name: "a"}))
3907}"#,
3908 );
3909 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3910 assert!(
3911 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
3912 "unexpected message: {}",
3913 warns[0]
3914 );
3915 }
3916
3917 #[test]
3918 fn test_interface_constraint_missing_method() {
3919 let warns = iface_errors(
3920 r#"pipeline t(task) {
3921 interface Sizable {
3922 fn size(self) -> int
3923 }
3924 struct Box { width: int }
3925 impl Box {
3926 fn area(self) -> int { return self.width }
3927 }
3928 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3929 measure(Box({width: 3}))
3930}"#,
3931 );
3932 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3933 assert!(
3934 warns[0].contains("missing method 'size'"),
3935 "unexpected message: {}",
3936 warns[0]
3937 );
3938 }
3939
3940 #[test]
3941 fn test_interface_constraint_param_count_mismatch() {
3942 let warns = iface_errors(
3943 r#"pipeline t(task) {
3944 interface Doubler {
3945 fn double(self, x: int) -> int
3946 }
3947 struct Bad { v: int }
3948 impl Bad {
3949 fn double(self) -> int { return self.v * 2 }
3950 }
3951 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3952 run_double(Bad({v: 5}))
3953}"#,
3954 );
3955 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3956 assert!(
3957 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3958 "unexpected message: {}",
3959 warns[0]
3960 );
3961 }
3962
3963 #[test]
3964 fn test_interface_constraint_satisfied() {
3965 let warns = iface_errors(
3966 r#"pipeline t(task) {
3967 interface Sizable {
3968 fn size(self) -> int
3969 }
3970 struct Box { width: int, height: int }
3971 impl Box {
3972 fn size(self) -> int { return self.width * self.height }
3973 }
3974 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3975 measure(Box({width: 3, height: 4}))
3976}"#,
3977 );
3978 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3979 }
3980
3981 #[test]
3982 fn test_interface_constraint_untyped_impl_compatible() {
3983 let warns = iface_errors(
3985 r#"pipeline t(task) {
3986 interface Sizable {
3987 fn size(self) -> int
3988 }
3989 struct Box { width: int }
3990 impl Box {
3991 fn size(self) { return self.width }
3992 }
3993 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3994 measure(Box({width: 3}))
3995}"#,
3996 );
3997 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3998 }
3999
4000 #[test]
4001 fn test_interface_constraint_int_float_covariance() {
4002 let warns = iface_errors(
4004 r#"pipeline t(task) {
4005 interface Measurable {
4006 fn value(self) -> float
4007 }
4008 struct Gauge { v: int }
4009 impl Gauge {
4010 fn value(self) -> int { return self.v }
4011 }
4012 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
4013 read_val(Gauge({v: 42}))
4014}"#,
4015 );
4016 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4017 }
4018
4019 #[test]
4022 fn test_nil_narrowing_then_branch() {
4023 let errs = errors(
4025 r#"pipeline t(task) {
4026 fn greet(name: string | nil) {
4027 if name != nil {
4028 let s: string = name
4029 }
4030 }
4031}"#,
4032 );
4033 assert!(errs.is_empty(), "got: {:?}", errs);
4034 }
4035
4036 #[test]
4037 fn test_nil_narrowing_else_branch() {
4038 let errs = errors(
4040 r#"pipeline t(task) {
4041 fn check(x: string | nil) {
4042 if x != nil {
4043 let s: string = x
4044 } else {
4045 let n: nil = x
4046 }
4047 }
4048}"#,
4049 );
4050 assert!(errs.is_empty(), "got: {:?}", errs);
4051 }
4052
4053 #[test]
4054 fn test_nil_equality_narrows_both() {
4055 let errs = errors(
4057 r#"pipeline t(task) {
4058 fn check(x: string | nil) {
4059 if x == nil {
4060 let n: nil = x
4061 } else {
4062 let s: string = x
4063 }
4064 }
4065}"#,
4066 );
4067 assert!(errs.is_empty(), "got: {:?}", errs);
4068 }
4069
4070 #[test]
4071 fn test_truthiness_narrowing() {
4072 let errs = errors(
4074 r#"pipeline t(task) {
4075 fn check(x: string | nil) {
4076 if x {
4077 let s: string = x
4078 }
4079 }
4080}"#,
4081 );
4082 assert!(errs.is_empty(), "got: {:?}", errs);
4083 }
4084
4085 #[test]
4086 fn test_negation_narrowing() {
4087 let errs = errors(
4089 r#"pipeline t(task) {
4090 fn check(x: string | nil) {
4091 if !x {
4092 let n: nil = x
4093 } else {
4094 let s: string = x
4095 }
4096 }
4097}"#,
4098 );
4099 assert!(errs.is_empty(), "got: {:?}", errs);
4100 }
4101
4102 #[test]
4103 fn test_typeof_narrowing() {
4104 let errs = errors(
4106 r#"pipeline t(task) {
4107 fn check(x: string | int) {
4108 if type_of(x) == "string" {
4109 let s: string = x
4110 }
4111 }
4112}"#,
4113 );
4114 assert!(errs.is_empty(), "got: {:?}", errs);
4115 }
4116
4117 #[test]
4118 fn test_typeof_narrowing_else() {
4119 let errs = errors(
4121 r#"pipeline t(task) {
4122 fn check(x: string | int) {
4123 if type_of(x) == "string" {
4124 let s: string = x
4125 } else {
4126 let i: int = x
4127 }
4128 }
4129}"#,
4130 );
4131 assert!(errs.is_empty(), "got: {:?}", errs);
4132 }
4133
4134 #[test]
4135 fn test_typeof_neq_narrowing() {
4136 let errs = errors(
4138 r#"pipeline t(task) {
4139 fn check(x: string | int) {
4140 if type_of(x) != "string" {
4141 let i: int = x
4142 } else {
4143 let s: string = x
4144 }
4145 }
4146}"#,
4147 );
4148 assert!(errs.is_empty(), "got: {:?}", errs);
4149 }
4150
4151 #[test]
4152 fn test_and_combines_narrowing() {
4153 let errs = errors(
4155 r#"pipeline t(task) {
4156 fn check(x: string | int | nil) {
4157 if x != nil && type_of(x) == "string" {
4158 let s: string = x
4159 }
4160 }
4161}"#,
4162 );
4163 assert!(errs.is_empty(), "got: {:?}", errs);
4164 }
4165
4166 #[test]
4167 fn test_or_falsy_narrowing() {
4168 let errs = errors(
4170 r#"pipeline t(task) {
4171 fn check(x: string | nil, y: int | nil) {
4172 if x || y {
4173 // conservative: can't narrow
4174 } else {
4175 let xn: nil = x
4176 let yn: nil = y
4177 }
4178 }
4179}"#,
4180 );
4181 assert!(errs.is_empty(), "got: {:?}", errs);
4182 }
4183
4184 #[test]
4185 fn test_guard_narrows_outer_scope() {
4186 let errs = errors(
4187 r#"pipeline t(task) {
4188 fn check(x: string | nil) {
4189 guard x != nil else { return }
4190 let s: string = x
4191 }
4192}"#,
4193 );
4194 assert!(errs.is_empty(), "got: {:?}", errs);
4195 }
4196
4197 #[test]
4198 fn test_while_narrows_body() {
4199 let errs = errors(
4200 r#"pipeline t(task) {
4201 fn check(x: string | nil) {
4202 while x != nil {
4203 let s: string = x
4204 break
4205 }
4206 }
4207}"#,
4208 );
4209 assert!(errs.is_empty(), "got: {:?}", errs);
4210 }
4211
4212 #[test]
4213 fn test_early_return_narrows_after_if() {
4214 let errs = errors(
4216 r#"pipeline t(task) {
4217 fn check(x: string | nil) -> string {
4218 if x == nil {
4219 return "default"
4220 }
4221 let s: string = x
4222 return s
4223 }
4224}"#,
4225 );
4226 assert!(errs.is_empty(), "got: {:?}", errs);
4227 }
4228
4229 #[test]
4230 fn test_early_throw_narrows_after_if() {
4231 let errs = errors(
4232 r#"pipeline t(task) {
4233 fn check(x: string | nil) {
4234 if x == nil {
4235 throw "missing"
4236 }
4237 let s: string = x
4238 }
4239}"#,
4240 );
4241 assert!(errs.is_empty(), "got: {:?}", errs);
4242 }
4243
4244 #[test]
4245 fn test_no_narrowing_unknown_type() {
4246 let errs = errors(
4248 r#"pipeline t(task) {
4249 fn check(x) {
4250 if x != nil {
4251 let s: string = x
4252 }
4253 }
4254}"#,
4255 );
4256 assert!(errs.is_empty(), "got: {:?}", errs);
4259 }
4260
4261 #[test]
4262 fn test_reassignment_invalidates_narrowing() {
4263 let errs = errors(
4265 r#"pipeline t(task) {
4266 fn check(x: string | nil) {
4267 var y: string | nil = x
4268 if y != nil {
4269 let s: string = y
4270 y = nil
4271 let s2: string = y
4272 }
4273 }
4274}"#,
4275 );
4276 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
4278 assert!(
4279 errs[0].contains("Type mismatch"),
4280 "expected type mismatch, got: {}",
4281 errs[0]
4282 );
4283 }
4284
4285 #[test]
4286 fn test_let_immutable_warning() {
4287 let all = check_source(
4288 r#"pipeline t(task) {
4289 let x = 42
4290 x = 43
4291}"#,
4292 );
4293 let warnings: Vec<_> = all
4294 .iter()
4295 .filter(|d| d.severity == DiagnosticSeverity::Warning)
4296 .collect();
4297 assert!(
4298 warnings.iter().any(|w| w.message.contains("immutable")),
4299 "expected immutability warning, got: {:?}",
4300 warnings
4301 );
4302 }
4303
4304 #[test]
4305 fn test_nested_narrowing() {
4306 let errs = errors(
4307 r#"pipeline t(task) {
4308 fn check(x: string | int | nil) {
4309 if x != nil {
4310 if type_of(x) == "int" {
4311 let i: int = x
4312 }
4313 }
4314 }
4315}"#,
4316 );
4317 assert!(errs.is_empty(), "got: {:?}", errs);
4318 }
4319
4320 #[test]
4321 fn test_match_narrows_arms() {
4322 let errs = errors(
4323 r#"pipeline t(task) {
4324 fn check(x: string | int) {
4325 match x {
4326 "hello" -> {
4327 let s: string = x
4328 }
4329 42 -> {
4330 let i: int = x
4331 }
4332 _ -> {}
4333 }
4334 }
4335}"#,
4336 );
4337 assert!(errs.is_empty(), "got: {:?}", errs);
4338 }
4339
4340 #[test]
4341 fn test_has_narrows_optional_field() {
4342 let errs = errors(
4343 r#"pipeline t(task) {
4344 fn check(x: {name?: string, age: int}) {
4345 if x.has("name") {
4346 let n: {name: string, age: int} = x
4347 }
4348 }
4349}"#,
4350 );
4351 assert!(errs.is_empty(), "got: {:?}", errs);
4352 }
4353
4354 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
4359 let mut lexer = Lexer::new(source);
4360 let tokens = lexer.tokenize().unwrap();
4361 let mut parser = Parser::new(tokens);
4362 let program = parser.parse().unwrap();
4363 TypeChecker::new().check_with_source(&program, source)
4364 }
4365
4366 #[test]
4367 fn test_fix_string_plus_int_literal() {
4368 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
4369 let diags = check_source_with_source(source);
4370 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4371 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
4372 let fix = fixable[0].fix.as_ref().unwrap();
4373 assert_eq!(fix.len(), 1);
4374 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
4375 }
4376
4377 #[test]
4378 fn test_fix_int_plus_string_literal() {
4379 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
4380 let diags = check_source_with_source(source);
4381 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4382 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
4383 let fix = fixable[0].fix.as_ref().unwrap();
4384 assert_eq!(fix[0].replacement, "\"${42}hello\"");
4385 }
4386
4387 #[test]
4388 fn test_fix_string_plus_variable() {
4389 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
4390 let diags = check_source_with_source(source);
4391 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4392 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
4393 let fix = fixable[0].fix.as_ref().unwrap();
4394 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
4395 }
4396
4397 #[test]
4398 fn test_no_fix_int_plus_int() {
4399 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}";
4401 let diags = check_source_with_source(source);
4402 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4403 assert!(
4404 fixable.is_empty(),
4405 "no fix expected for numeric ops, got: {fixable:?}"
4406 );
4407 }
4408
4409 #[test]
4410 fn test_no_fix_without_source() {
4411 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
4412 let diags = check_source(source);
4413 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
4414 assert!(
4415 fixable.is_empty(),
4416 "without source, no fix should be generated"
4417 );
4418 }
4419
4420 #[test]
4423 fn test_union_exhaustive_match_no_warning() {
4424 let warns = warnings(
4425 r#"pipeline t(task) {
4426 let x: string | int | nil = nil
4427 match x {
4428 "hello" -> { log("s") }
4429 42 -> { log("i") }
4430 nil -> { log("n") }
4431 }
4432}"#,
4433 );
4434 let union_warns: Vec<_> = warns
4435 .iter()
4436 .filter(|w| w.contains("Non-exhaustive match on union"))
4437 .collect();
4438 assert!(union_warns.is_empty());
4439 }
4440
4441 #[test]
4442 fn test_union_non_exhaustive_match_warning() {
4443 let warns = warnings(
4444 r#"pipeline t(task) {
4445 let x: string | int | nil = nil
4446 match x {
4447 "hello" -> { log("s") }
4448 42 -> { log("i") }
4449 }
4450}"#,
4451 );
4452 let union_warns: Vec<_> = warns
4453 .iter()
4454 .filter(|w| w.contains("Non-exhaustive match on union"))
4455 .collect();
4456 assert_eq!(union_warns.len(), 1);
4457 assert!(union_warns[0].contains("nil"));
4458 }
4459
4460 #[test]
4463 fn test_nil_coalesce_non_union_preserves_left_type() {
4464 let errs = errors(
4466 r#"pipeline t(task) {
4467 let x: int = 42
4468 let y: int = x ?? 0
4469}"#,
4470 );
4471 assert!(errs.is_empty());
4472 }
4473
4474 #[test]
4475 fn test_nil_coalesce_nil_returns_right_type() {
4476 let errs = errors(
4477 r#"pipeline t(task) {
4478 let x: string = nil ?? "fallback"
4479}"#,
4480 );
4481 assert!(errs.is_empty());
4482 }
4483
4484 #[test]
4487 fn test_never_is_subtype_of_everything() {
4488 let tc = TypeChecker::new();
4489 let scope = TypeScope::new();
4490 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &TypeExpr::Never, &scope));
4491 assert!(tc.types_compatible(&TypeExpr::Named("int".into()), &TypeExpr::Never, &scope));
4492 assert!(tc.types_compatible(
4493 &TypeExpr::Union(vec![
4494 TypeExpr::Named("string".into()),
4495 TypeExpr::Named("nil".into()),
4496 ]),
4497 &TypeExpr::Never,
4498 &scope,
4499 ));
4500 }
4501
4502 #[test]
4503 fn test_nothing_is_subtype_of_never() {
4504 let tc = TypeChecker::new();
4505 let scope = TypeScope::new();
4506 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("string".into()), &scope));
4507 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("int".into()), &scope));
4508 }
4509
4510 #[test]
4511 fn test_never_never_compatible() {
4512 let tc = TypeChecker::new();
4513 let scope = TypeScope::new();
4514 assert!(tc.types_compatible(&TypeExpr::Never, &TypeExpr::Never, &scope));
4515 }
4516
4517 #[test]
4518 fn test_simplify_union_removes_never() {
4519 assert_eq!(
4520 simplify_union(vec![TypeExpr::Never, TypeExpr::Named("string".into())]),
4521 TypeExpr::Named("string".into()),
4522 );
4523 assert_eq!(
4524 simplify_union(vec![TypeExpr::Never, TypeExpr::Never]),
4525 TypeExpr::Never,
4526 );
4527 assert_eq!(
4528 simplify_union(vec![
4529 TypeExpr::Named("string".into()),
4530 TypeExpr::Never,
4531 TypeExpr::Named("int".into()),
4532 ]),
4533 TypeExpr::Union(vec![
4534 TypeExpr::Named("string".into()),
4535 TypeExpr::Named("int".into()),
4536 ]),
4537 );
4538 }
4539
4540 #[test]
4541 fn test_remove_from_union_exhausted_returns_never() {
4542 let result = remove_from_union(&[TypeExpr::Named("string".into())], "string");
4543 assert_eq!(result, Some(TypeExpr::Never));
4544 }
4545
4546 #[test]
4547 fn test_if_else_one_branch_throws_infers_other() {
4548 let errs = errors(
4550 r#"pipeline t(task) {
4551 fn foo(x: bool) -> int {
4552 let result: int = if x { 42 } else { throw "err" }
4553 return result
4554 }
4555}"#,
4556 );
4557 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4558 }
4559
4560 #[test]
4561 fn test_if_else_both_branches_throw_infers_never() {
4562 let errs = errors(
4564 r#"pipeline t(task) {
4565 fn foo(x: bool) -> string {
4566 let result: string = if x { throw "a" } else { throw "b" }
4567 return result
4568 }
4569}"#,
4570 );
4571 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4572 }
4573
4574 #[test]
4577 fn test_unreachable_after_return() {
4578 let warns = warnings(
4579 r#"pipeline t(task) {
4580 fn foo() -> int {
4581 return 1
4582 let x = 2
4583 }
4584}"#,
4585 );
4586 assert!(
4587 warns.iter().any(|w| w.contains("unreachable")),
4588 "expected unreachable warning: {warns:?}"
4589 );
4590 }
4591
4592 #[test]
4593 fn test_unreachable_after_throw() {
4594 let warns = warnings(
4595 r#"pipeline t(task) {
4596 fn foo() {
4597 throw "err"
4598 let x = 2
4599 }
4600}"#,
4601 );
4602 assert!(
4603 warns.iter().any(|w| w.contains("unreachable")),
4604 "expected unreachable warning: {warns:?}"
4605 );
4606 }
4607
4608 #[test]
4609 fn test_unreachable_after_composite_exit() {
4610 let warns = warnings(
4611 r#"pipeline t(task) {
4612 fn foo(x: bool) {
4613 if x { return 1 } else { throw "err" }
4614 let y = 2
4615 }
4616}"#,
4617 );
4618 assert!(
4619 warns.iter().any(|w| w.contains("unreachable")),
4620 "expected unreachable warning: {warns:?}"
4621 );
4622 }
4623
4624 #[test]
4625 fn test_no_unreachable_warning_when_reachable() {
4626 let warns = warnings(
4627 r#"pipeline t(task) {
4628 fn foo(x: bool) {
4629 if x { return 1 }
4630 let y = 2
4631 }
4632}"#,
4633 );
4634 assert!(
4635 !warns.iter().any(|w| w.contains("unreachable")),
4636 "unexpected unreachable warning: {warns:?}"
4637 );
4638 }
4639
4640 #[test]
4643 fn test_catch_typed_error_variable() {
4644 let errs = errors(
4646 r#"pipeline t(task) {
4647 enum AppError { NotFound, Timeout }
4648 try {
4649 throw AppError.NotFound
4650 } catch (e: AppError) {
4651 let x: AppError = e
4652 }
4653}"#,
4654 );
4655 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4656 }
4657
4658 #[test]
4661 fn test_unreachable_with_never_arg_no_error() {
4662 let errs = errors(
4664 r#"pipeline t(task) {
4665 fn foo(x: string | int) {
4666 if type_of(x) == "string" { return }
4667 if type_of(x) == "int" { return }
4668 unreachable(x)
4669 }
4670}"#,
4671 );
4672 assert!(
4673 !errs.iter().any(|e| e.contains("unreachable")),
4674 "unexpected unreachable error: {errs:?}"
4675 );
4676 }
4677
4678 #[test]
4679 fn test_unreachable_with_remaining_types_errors() {
4680 let errs = errors(
4682 r#"pipeline t(task) {
4683 fn foo(x: string | int | nil) {
4684 if type_of(x) == "string" { return }
4685 unreachable(x)
4686 }
4687}"#,
4688 );
4689 assert!(
4690 errs.iter()
4691 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
4692 "expected unreachable error about remaining types: {errs:?}"
4693 );
4694 }
4695
4696 #[test]
4697 fn test_unreachable_no_args_no_compile_error() {
4698 let errs = errors(
4699 r#"pipeline t(task) {
4700 fn foo() {
4701 unreachable()
4702 }
4703}"#,
4704 );
4705 assert!(
4706 !errs
4707 .iter()
4708 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
4709 "unreachable() with no args should not produce type error: {errs:?}"
4710 );
4711 }
4712
4713 #[test]
4714 fn test_never_type_annotation_parses() {
4715 let errs = errors(
4716 r#"pipeline t(task) {
4717 fn foo() -> never {
4718 throw "always throws"
4719 }
4720}"#,
4721 );
4722 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
4723 }
4724
4725 #[test]
4726 fn test_format_type_never() {
4727 assert_eq!(format_type(&TypeExpr::Never), "never");
4728 }
4729}