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