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 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
3437 ("int", "int") => Some(TypeExpr::Named("int".into())),
3438 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3439 _ => None,
3440 }
3441 }
3442 _ => None,
3443 },
3444 "??" => match (left, right) {
3445 (Some(TypeExpr::Union(members)), _) => {
3447 let non_nil: Vec<_> = members
3448 .iter()
3449 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
3450 .cloned()
3451 .collect();
3452 if non_nil.len() == 1 {
3453 Some(non_nil[0].clone())
3454 } else if non_nil.is_empty() {
3455 right.clone()
3456 } else {
3457 Some(TypeExpr::Union(non_nil))
3458 }
3459 }
3460 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
3462 (Some(l), _) => Some(l.clone()),
3464 (None, _) => right.clone(),
3466 },
3467 "|>" => None,
3468 _ => None,
3469 }
3470}
3471
3472pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
3477 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
3478 let mut details = Vec::new();
3479 for field in ef {
3480 if field.optional {
3481 continue;
3482 }
3483 match af.iter().find(|f| f.name == field.name) {
3484 None => details.push(format!(
3485 "missing field '{}' ({})",
3486 field.name,
3487 format_type(&field.type_expr)
3488 )),
3489 Some(actual_field) => {
3490 let e_str = format_type(&field.type_expr);
3491 let a_str = format_type(&actual_field.type_expr);
3492 if e_str != a_str {
3493 details.push(format!(
3494 "field '{}' has type {}, expected {}",
3495 field.name, a_str, e_str
3496 ));
3497 }
3498 }
3499 }
3500 }
3501 if details.is_empty() {
3502 None
3503 } else {
3504 Some(details.join("; "))
3505 }
3506 } else {
3507 None
3508 }
3509}
3510
3511fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
3514 matches!(
3515 &value.node,
3516 Node::IntLiteral(_)
3517 | Node::FloatLiteral(_)
3518 | Node::StringLiteral(_)
3519 | Node::BoolLiteral(_)
3520 | Node::NilLiteral
3521 | Node::ListLiteral(_)
3522 | Node::DictLiteral(_)
3523 | Node::InterpolatedString(_)
3524 )
3525}
3526
3527pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
3530 match &stmt.node {
3531 Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
3532 true
3533 }
3534 Node::IfElse {
3535 then_body,
3536 else_body: Some(else_body),
3537 ..
3538 } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
3539 _ => false,
3540 }
3541}
3542
3543pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
3545 stmts.iter().any(stmt_definitely_exits)
3546}
3547
3548pub fn format_type(ty: &TypeExpr) -> String {
3549 match ty {
3550 TypeExpr::Named(n) => n.clone(),
3551 TypeExpr::Union(types) => types
3552 .iter()
3553 .map(format_type)
3554 .collect::<Vec<_>>()
3555 .join(" | "),
3556 TypeExpr::Shape(fields) => {
3557 let inner: Vec<String> = fields
3558 .iter()
3559 .map(|f| {
3560 let opt = if f.optional { "?" } else { "" };
3561 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
3562 })
3563 .collect();
3564 format!("{{{}}}", inner.join(", "))
3565 }
3566 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
3567 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
3568 TypeExpr::Applied { name, args } => {
3569 let args_str = args.iter().map(format_type).collect::<Vec<_>>().join(", ");
3570 format!("{name}<{args_str}>")
3571 }
3572 TypeExpr::FnType {
3573 params,
3574 return_type,
3575 } => {
3576 let params_str = params
3577 .iter()
3578 .map(format_type)
3579 .collect::<Vec<_>>()
3580 .join(", ");
3581 format!("fn({}) -> {}", params_str, format_type(return_type))
3582 }
3583 TypeExpr::Never => "never".to_string(),
3584 }
3585}
3586
3587fn simplify_union(members: Vec<TypeExpr>) -> TypeExpr {
3589 let filtered: Vec<TypeExpr> = members
3590 .into_iter()
3591 .filter(|m| !matches!(m, TypeExpr::Never))
3592 .collect();
3593 match filtered.len() {
3594 0 => TypeExpr::Never,
3595 1 => filtered.into_iter().next().unwrap(),
3596 _ => TypeExpr::Union(filtered),
3597 }
3598}
3599
3600fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
3603 let remaining: Vec<TypeExpr> = members
3604 .iter()
3605 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
3606 .cloned()
3607 .collect();
3608 match remaining.len() {
3609 0 => Some(TypeExpr::Never),
3610 1 => Some(remaining.into_iter().next().unwrap()),
3611 _ => Some(TypeExpr::Union(remaining)),
3612 }
3613}
3614
3615fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
3617 if members
3618 .iter()
3619 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
3620 {
3621 Some(TypeExpr::Named(target.to_string()))
3622 } else {
3623 None
3624 }
3625}
3626
3627fn extract_type_of_var(node: &SNode) -> Option<String> {
3629 if let Node::FunctionCall { name, args } = &node.node {
3630 if name == "type_of" && args.len() == 1 {
3631 if let Node::Identifier(var) = &args[0].node {
3632 return Some(var.clone());
3633 }
3634 }
3635 }
3636 None
3637}
3638
3639fn schema_type_expr_from_node(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3640 match &node.node {
3641 Node::Identifier(name) => scope.get_schema_binding(name).cloned().flatten(),
3642 Node::DictLiteral(entries) => schema_type_expr_from_dict(entries, scope),
3643 _ => None,
3644 }
3645}
3646
3647fn schema_type_expr_from_dict(entries: &[DictEntry], scope: &TypeScope) -> Option<TypeExpr> {
3648 let mut type_name: Option<String> = None;
3649 let mut properties: Option<&SNode> = None;
3650 let mut required: Option<Vec<String>> = None;
3651 let mut items: Option<&SNode> = None;
3652 let mut union: Option<&SNode> = None;
3653 let mut nullable = false;
3654 let mut additional_properties: Option<&SNode> = None;
3655
3656 for entry in entries {
3657 let key = schema_entry_key(&entry.key)?;
3658 match key.as_str() {
3659 "type" => match &entry.value.node {
3660 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3661 type_name = Some(normalize_schema_type_name(text));
3662 }
3663 Node::ListLiteral(items_list) => {
3664 let union_members = items_list
3665 .iter()
3666 .filter_map(|item| match &item.node {
3667 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3668 Some(TypeExpr::Named(normalize_schema_type_name(text)))
3669 }
3670 _ => None,
3671 })
3672 .collect::<Vec<_>>();
3673 if !union_members.is_empty() {
3674 return Some(TypeExpr::Union(union_members));
3675 }
3676 }
3677 _ => {}
3678 },
3679 "properties" => properties = Some(&entry.value),
3680 "required" => {
3681 required = schema_required_names(&entry.value);
3682 }
3683 "items" => items = Some(&entry.value),
3684 "union" | "oneOf" | "anyOf" => union = Some(&entry.value),
3685 "nullable" => {
3686 nullable = matches!(entry.value.node, Node::BoolLiteral(true));
3687 }
3688 "additional_properties" | "additionalProperties" => {
3689 additional_properties = Some(&entry.value);
3690 }
3691 _ => {}
3692 }
3693 }
3694
3695 let mut schema_type = if let Some(union_node) = union {
3696 schema_union_type_expr(union_node, scope)?
3697 } else if let Some(properties_node) = properties {
3698 let property_entries = match &properties_node.node {
3699 Node::DictLiteral(entries) => entries,
3700 _ => return None,
3701 };
3702 let required_names = required.unwrap_or_default();
3703 let mut fields = Vec::new();
3704 for entry in property_entries {
3705 let field_name = schema_entry_key(&entry.key)?;
3706 let field_type = schema_type_expr_from_node(&entry.value, scope)?;
3707 fields.push(ShapeField {
3708 name: field_name.clone(),
3709 type_expr: field_type,
3710 optional: !required_names.contains(&field_name),
3711 });
3712 }
3713 TypeExpr::Shape(fields)
3714 } else if let Some(item_node) = items {
3715 TypeExpr::List(Box::new(schema_type_expr_from_node(item_node, scope)?))
3716 } else if let Some(type_name) = type_name {
3717 if type_name == "dict" {
3718 if let Some(extra_node) = additional_properties {
3719 let value_type = match &extra_node.node {
3720 Node::BoolLiteral(_) => None,
3721 _ => schema_type_expr_from_node(extra_node, scope),
3722 };
3723 if let Some(value_type) = value_type {
3724 TypeExpr::DictType(
3725 Box::new(TypeExpr::Named("string".into())),
3726 Box::new(value_type),
3727 )
3728 } else {
3729 TypeExpr::Named(type_name)
3730 }
3731 } else {
3732 TypeExpr::Named(type_name)
3733 }
3734 } else {
3735 TypeExpr::Named(type_name)
3736 }
3737 } else {
3738 return None;
3739 };
3740
3741 if nullable {
3742 schema_type = match schema_type {
3743 TypeExpr::Union(mut members) => {
3744 if !members
3745 .iter()
3746 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil"))
3747 {
3748 members.push(TypeExpr::Named("nil".into()));
3749 }
3750 TypeExpr::Union(members)
3751 }
3752 other => TypeExpr::Union(vec![other, TypeExpr::Named("nil".into())]),
3753 };
3754 }
3755
3756 Some(schema_type)
3757}
3758
3759fn schema_union_type_expr(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3760 let Node::ListLiteral(items) = &node.node else {
3761 return None;
3762 };
3763 let members = items
3764 .iter()
3765 .filter_map(|item| schema_type_expr_from_node(item, scope))
3766 .collect::<Vec<_>>();
3767 match members.len() {
3768 0 => None,
3769 1 => members.into_iter().next(),
3770 _ => Some(TypeExpr::Union(members)),
3771 }
3772}
3773
3774fn schema_required_names(node: &SNode) -> Option<Vec<String>> {
3775 let Node::ListLiteral(items) = &node.node else {
3776 return None;
3777 };
3778 Some(
3779 items
3780 .iter()
3781 .filter_map(|item| match &item.node {
3782 Node::StringLiteral(text) | Node::RawStringLiteral(text) => Some(text.clone()),
3783 Node::Identifier(text) => Some(text.clone()),
3784 _ => None,
3785 })
3786 .collect(),
3787 )
3788}
3789
3790fn schema_entry_key(node: &SNode) -> Option<String> {
3791 match &node.node {
3792 Node::Identifier(name) => Some(name.clone()),
3793 Node::StringLiteral(name) | Node::RawStringLiteral(name) => Some(name.clone()),
3794 _ => None,
3795 }
3796}
3797
3798fn normalize_schema_type_name(text: &str) -> String {
3799 match text {
3800 "object" => "dict".into(),
3801 "array" => "list".into(),
3802 "integer" => "int".into(),
3803 "number" => "float".into(),
3804 "boolean" => "bool".into(),
3805 "null" => "nil".into(),
3806 other => other.into(),
3807 }
3808}
3809
3810fn intersect_types(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
3811 match (current, schema_type) {
3812 (TypeExpr::Union(members), other) => {
3813 let kept = members
3814 .iter()
3815 .filter_map(|member| intersect_types(member, other))
3816 .collect::<Vec<_>>();
3817 match kept.len() {
3818 0 => None,
3819 1 => kept.into_iter().next(),
3820 _ => Some(TypeExpr::Union(kept)),
3821 }
3822 }
3823 (other, TypeExpr::Union(members)) => {
3824 let kept = members
3825 .iter()
3826 .filter_map(|member| intersect_types(other, member))
3827 .collect::<Vec<_>>();
3828 match kept.len() {
3829 0 => None,
3830 1 => kept.into_iter().next(),
3831 _ => Some(TypeExpr::Union(kept)),
3832 }
3833 }
3834 (TypeExpr::Named(left), TypeExpr::Named(right)) if left == right => {
3835 Some(TypeExpr::Named(left.clone()))
3836 }
3837 (TypeExpr::Named(name), TypeExpr::Shape(fields)) if name == "dict" => {
3838 Some(TypeExpr::Shape(fields.clone()))
3839 }
3840 (TypeExpr::Shape(fields), TypeExpr::Named(name)) if name == "dict" => {
3841 Some(TypeExpr::Shape(fields.clone()))
3842 }
3843 (TypeExpr::Named(name), TypeExpr::List(inner)) if name == "list" => {
3844 Some(TypeExpr::List(inner.clone()))
3845 }
3846 (TypeExpr::List(inner), TypeExpr::Named(name)) if name == "list" => {
3847 Some(TypeExpr::List(inner.clone()))
3848 }
3849 (TypeExpr::Named(name), TypeExpr::DictType(key, value)) if name == "dict" => {
3850 Some(TypeExpr::DictType(key.clone(), value.clone()))
3851 }
3852 (TypeExpr::DictType(key, value), TypeExpr::Named(name)) if name == "dict" => {
3853 Some(TypeExpr::DictType(key.clone(), value.clone()))
3854 }
3855 (TypeExpr::Shape(_), TypeExpr::Shape(fields)) => Some(TypeExpr::Shape(fields.clone())),
3856 (TypeExpr::List(current_inner), TypeExpr::List(schema_inner)) => {
3857 intersect_types(current_inner, schema_inner)
3858 .map(|inner| TypeExpr::List(Box::new(inner)))
3859 }
3860 (
3861 TypeExpr::DictType(current_key, current_value),
3862 TypeExpr::DictType(schema_key, schema_value),
3863 ) => {
3864 let key = intersect_types(current_key, schema_key)?;
3865 let value = intersect_types(current_value, schema_value)?;
3866 Some(TypeExpr::DictType(Box::new(key), Box::new(value)))
3867 }
3868 _ => None,
3869 }
3870}
3871
3872fn subtract_type(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
3873 match current {
3874 TypeExpr::Union(members) => {
3875 let remaining = members
3876 .iter()
3877 .filter(|member| intersect_types(member, schema_type).is_none())
3878 .cloned()
3879 .collect::<Vec<_>>();
3880 match remaining.len() {
3881 0 => None,
3882 1 => remaining.into_iter().next(),
3883 _ => Some(TypeExpr::Union(remaining)),
3884 }
3885 }
3886 other if intersect_types(other, schema_type).is_some() => None,
3887 other => Some(other.clone()),
3888 }
3889}
3890
3891fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
3893 for (var_name, narrowed_type) in refinements {
3894 if !scope.narrowed_vars.contains_key(var_name) {
3896 if let Some(original) = scope.get_var(var_name).cloned() {
3897 scope.narrowed_vars.insert(var_name.clone(), original);
3898 }
3899 }
3900 scope.define_var(var_name, narrowed_type.clone());
3901 }
3902}
3903
3904#[cfg(test)]
3905mod tests {
3906 use super::*;
3907 use crate::Parser;
3908 use harn_lexer::Lexer;
3909
3910 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
3911 let mut lexer = Lexer::new(source);
3912 let tokens = lexer.tokenize().unwrap();
3913 let mut parser = Parser::new(tokens);
3914 let program = parser.parse().unwrap();
3915 TypeChecker::new().check(&program)
3916 }
3917
3918 fn errors(source: &str) -> Vec<String> {
3919 check_source(source)
3920 .into_iter()
3921 .filter(|d| d.severity == DiagnosticSeverity::Error)
3922 .map(|d| d.message)
3923 .collect()
3924 }
3925
3926 #[test]
3927 fn test_no_errors_for_untyped_code() {
3928 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
3929 assert!(errs.is_empty());
3930 }
3931
3932 #[test]
3933 fn test_correct_typed_let() {
3934 let errs = errors("pipeline t(task) { let x: int = 42 }");
3935 assert!(errs.is_empty());
3936 }
3937
3938 #[test]
3939 fn test_type_mismatch_let() {
3940 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
3941 assert_eq!(errs.len(), 1);
3942 assert!(errs[0].contains("Type mismatch"));
3943 assert!(errs[0].contains("int"));
3944 assert!(errs[0].contains("string"));
3945 }
3946
3947 #[test]
3948 fn test_correct_typed_fn() {
3949 let errs = errors(
3950 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
3951 );
3952 assert!(errs.is_empty());
3953 }
3954
3955 #[test]
3956 fn test_fn_arg_type_mismatch() {
3957 let errs = errors(
3958 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
3959add("hello", 2) }"#,
3960 );
3961 assert_eq!(errs.len(), 1);
3962 assert!(errs[0].contains("Argument 1"));
3963 assert!(errs[0].contains("expected int"));
3964 }
3965
3966 #[test]
3967 fn test_return_type_mismatch() {
3968 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
3969 assert_eq!(errs.len(), 1);
3970 assert!(errs[0].contains("Return type mismatch"));
3971 }
3972
3973 #[test]
3974 fn test_union_type_compatible() {
3975 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
3976 assert!(errs.is_empty());
3977 }
3978
3979 #[test]
3980 fn test_union_type_mismatch() {
3981 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
3982 assert_eq!(errs.len(), 1);
3983 assert!(errs[0].contains("Type mismatch"));
3984 }
3985
3986 #[test]
3987 fn test_type_inference_propagation() {
3988 let errs = errors(
3989 r#"pipeline t(task) {
3990 fn add(a: int, b: int) -> int { return a + b }
3991 let result: string = add(1, 2)
3992}"#,
3993 );
3994 assert_eq!(errs.len(), 1);
3995 assert!(errs[0].contains("Type mismatch"));
3996 assert!(errs[0].contains("string"));
3997 assert!(errs[0].contains("int"));
3998 }
3999
4000 #[test]
4001 fn test_generic_return_type_instantiates_from_callsite() {
4002 let errs = errors(
4003 r#"pipeline t(task) {
4004 fn identity<T>(x: T) -> T { return x }
4005 fn first<T>(items: list<T>) -> T { return items[0] }
4006 let n: int = identity(42)
4007 let s: string = first(["a", "b"])
4008}"#,
4009 );
4010 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4011 }
4012
4013 #[test]
4014 fn test_generic_type_param_must_bind_consistently() {
4015 let errs = errors(
4016 r#"pipeline t(task) {
4017 fn keep<T>(a: T, b: T) -> T { return a }
4018 keep(1, "x")
4019}"#,
4020 );
4021 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
4022 assert!(
4023 errs.iter()
4024 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
4025 "missing generic binding conflict error: {:?}",
4026 errs
4027 );
4028 assert!(
4029 errs.iter()
4030 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
4031 "missing instantiated argument mismatch error: {:?}",
4032 errs
4033 );
4034 }
4035
4036 #[test]
4037 fn test_generic_list_binding_propagates_element_type() {
4038 let errs = errors(
4039 r#"pipeline t(task) {
4040 fn first<T>(items: list<T>) -> T { return items[0] }
4041 let bad: string = first([1, 2, 3])
4042}"#,
4043 );
4044 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
4045 assert!(errs[0].contains("declared as string, but assigned int"));
4046 }
4047
4048 #[test]
4049 fn test_generic_struct_literal_instantiates_type_arguments() {
4050 let errs = errors(
4051 r#"pipeline t(task) {
4052 struct Pair<A, B> {
4053 first: A
4054 second: B
4055 }
4056 let pair: Pair<int, string> = Pair { first: 1, second: "two" }
4057}"#,
4058 );
4059 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4060 }
4061
4062 #[test]
4063 fn test_generic_enum_construct_instantiates_type_arguments() {
4064 let errs = errors(
4065 r#"pipeline t(task) {
4066 enum Option<T> {
4067 Some(value: T),
4068 None
4069 }
4070 let value: Option<int> = Option.Some(42)
4071}"#,
4072 );
4073 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4074 }
4075
4076 #[test]
4077 fn test_result_generic_type_compatibility() {
4078 let errs = errors(
4079 r#"pipeline t(task) {
4080 let ok: Result<int, string> = Result.Ok(42)
4081 let err: Result<int, string> = Result.Err("oops")
4082}"#,
4083 );
4084 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4085 }
4086
4087 #[test]
4088 fn test_result_generic_type_mismatch_reports_error() {
4089 let errs = errors(
4090 r#"pipeline t(task) {
4091 let bad: Result<int, string> = Result.Err(42)
4092}"#,
4093 );
4094 assert_eq!(errs.len(), 1, "expected 1 error, got: {errs:?}");
4095 assert!(errs[0].contains("Result<int, string>"));
4096 assert!(errs[0].contains("Result<_, int>"));
4097 }
4098
4099 #[test]
4100 fn test_builtin_return_type_inference() {
4101 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
4102 assert_eq!(errs.len(), 1);
4103 assert!(errs[0].contains("string"));
4104 assert!(errs[0].contains("int"));
4105 }
4106
4107 #[test]
4108 fn test_workflow_and_transcript_builtins_are_known() {
4109 let errs = errors(
4110 r#"pipeline t(task) {
4111 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
4112 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
4113 let run: dict = workflow_execute("task", flow, [], {})
4114 let tree: dict = load_run_tree("run.json")
4115 let fixture: dict = run_record_fixture(run?.run)
4116 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
4117 let diff: dict = run_record_diff(run?.run, run?.run)
4118 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
4119 let suite_report: dict = eval_suite_run(manifest)
4120 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
4121 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
4122 let selection: dict = artifact_editor_selection("src/main.rs", "main")
4123 let verify: dict = artifact_verification_result("verify", "ok")
4124 let test_result: dict = artifact_test_result("tests", "pass")
4125 let cmd: dict = artifact_command_result("cargo test", {status: 0})
4126 let patch: dict = artifact_diff("src/main.rs", "old", "new")
4127 let git: dict = artifact_git_diff("diff --git a b")
4128 let review: dict = artifact_diff_review(patch, "review me")
4129 let decision: dict = artifact_review_decision(review, "accepted")
4130 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
4131 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
4132 let apply: dict = artifact_apply_intent(review, "apply")
4133 let transcript = transcript_reset({metadata: {source: "test"}})
4134 let visible: string = transcript_render_visible(transcript_archive(transcript))
4135 let events: list = transcript_events(transcript)
4136 let context: string = artifact_context([], {max_artifacts: 1})
4137 println(report)
4138 println(run)
4139 println(tree)
4140 println(fixture)
4141 println(suite)
4142 println(diff)
4143 println(manifest)
4144 println(suite_report)
4145 println(wf)
4146 println(snap)
4147 println(selection)
4148 println(verify)
4149 println(test_result)
4150 println(cmd)
4151 println(patch)
4152 println(git)
4153 println(review)
4154 println(decision)
4155 println(proposal)
4156 println(bundle)
4157 println(apply)
4158 println(visible)
4159 println(events)
4160 println(context)
4161}"#,
4162 );
4163 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4164 }
4165
4166 #[test]
4167 fn test_binary_op_type_inference() {
4168 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
4169 assert_eq!(errs.len(), 1);
4170 }
4171
4172 #[test]
4173 fn test_comparison_returns_bool() {
4174 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
4175 assert!(errs.is_empty());
4176 }
4177
4178 #[test]
4179 fn test_int_float_promotion() {
4180 let errs = errors("pipeline t(task) { let x: float = 42 }");
4181 assert!(errs.is_empty());
4182 }
4183
4184 #[test]
4185 fn test_untyped_code_no_errors() {
4186 let errs = errors(
4187 r#"pipeline t(task) {
4188 fn process(data) {
4189 let result = data + " processed"
4190 return result
4191 }
4192 log(process("hello"))
4193}"#,
4194 );
4195 assert!(errs.is_empty());
4196 }
4197
4198 #[test]
4199 fn test_type_alias() {
4200 let errs = errors(
4201 r#"pipeline t(task) {
4202 type Name = string
4203 let x: Name = "hello"
4204}"#,
4205 );
4206 assert!(errs.is_empty());
4207 }
4208
4209 #[test]
4210 fn test_type_alias_mismatch() {
4211 let errs = errors(
4212 r#"pipeline t(task) {
4213 type Name = string
4214 let x: Name = 42
4215}"#,
4216 );
4217 assert_eq!(errs.len(), 1);
4218 }
4219
4220 #[test]
4221 fn test_assignment_type_check() {
4222 let errs = errors(
4223 r#"pipeline t(task) {
4224 var x: int = 0
4225 x = "hello"
4226}"#,
4227 );
4228 assert_eq!(errs.len(), 1);
4229 assert!(errs[0].contains("cannot assign string"));
4230 }
4231
4232 #[test]
4233 fn test_covariance_int_to_float_in_fn() {
4234 let errs = errors(
4235 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
4236 );
4237 assert!(errs.is_empty());
4238 }
4239
4240 #[test]
4241 fn test_covariance_return_type() {
4242 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
4243 assert!(errs.is_empty());
4244 }
4245
4246 #[test]
4247 fn test_no_contravariance_float_to_int() {
4248 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
4249 assert_eq!(errs.len(), 1);
4250 }
4251
4252 fn warnings(source: &str) -> Vec<String> {
4255 check_source(source)
4256 .into_iter()
4257 .filter(|d| d.severity == DiagnosticSeverity::Warning)
4258 .map(|d| d.message)
4259 .collect()
4260 }
4261
4262 #[test]
4263 fn test_exhaustive_match_no_warning() {
4264 let warns = warnings(
4265 r#"pipeline t(task) {
4266 enum Color { Red, Green, Blue }
4267 let c = Color.Red
4268 match c.variant {
4269 "Red" -> { log("r") }
4270 "Green" -> { log("g") }
4271 "Blue" -> { log("b") }
4272 }
4273}"#,
4274 );
4275 let exhaustive_warns: Vec<_> = warns
4276 .iter()
4277 .filter(|w| w.contains("Non-exhaustive"))
4278 .collect();
4279 assert!(exhaustive_warns.is_empty());
4280 }
4281
4282 #[test]
4283 fn test_non_exhaustive_match_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 }
4292}"#,
4293 );
4294 let exhaustive_warns: Vec<_> = warns
4295 .iter()
4296 .filter(|w| w.contains("Non-exhaustive"))
4297 .collect();
4298 assert_eq!(exhaustive_warns.len(), 1);
4299 assert!(exhaustive_warns[0].contains("Blue"));
4300 }
4301
4302 #[test]
4303 fn test_non_exhaustive_multiple_missing() {
4304 let warns = warnings(
4305 r#"pipeline t(task) {
4306 enum Status { Active, Inactive, Pending }
4307 let s = Status.Active
4308 match s.variant {
4309 "Active" -> { log("a") }
4310 }
4311}"#,
4312 );
4313 let exhaustive_warns: Vec<_> = warns
4314 .iter()
4315 .filter(|w| w.contains("Non-exhaustive"))
4316 .collect();
4317 assert_eq!(exhaustive_warns.len(), 1);
4318 assert!(exhaustive_warns[0].contains("Inactive"));
4319 assert!(exhaustive_warns[0].contains("Pending"));
4320 }
4321
4322 #[test]
4323 fn test_enum_construct_type_inference() {
4324 let errs = errors(
4325 r#"pipeline t(task) {
4326 enum Color { Red, Green, Blue }
4327 let c: Color = Color.Red
4328}"#,
4329 );
4330 assert!(errs.is_empty());
4331 }
4332
4333 #[test]
4336 fn test_nil_coalescing_strips_nil() {
4337 let errs = errors(
4339 r#"pipeline t(task) {
4340 let x: string | nil = nil
4341 let y: string = x ?? "default"
4342}"#,
4343 );
4344 assert!(errs.is_empty());
4345 }
4346
4347 #[test]
4348 fn test_shape_mismatch_detail_missing_field() {
4349 let errs = errors(
4350 r#"pipeline t(task) {
4351 let x: {name: string, age: int} = {name: "hello"}
4352}"#,
4353 );
4354 assert_eq!(errs.len(), 1);
4355 assert!(
4356 errs[0].contains("missing field 'age'"),
4357 "expected detail about missing field, got: {}",
4358 errs[0]
4359 );
4360 }
4361
4362 #[test]
4363 fn test_shape_mismatch_detail_wrong_type() {
4364 let errs = errors(
4365 r#"pipeline t(task) {
4366 let x: {name: string, age: int} = {name: 42, age: 10}
4367}"#,
4368 );
4369 assert_eq!(errs.len(), 1);
4370 assert!(
4371 errs[0].contains("field 'name' has type int, expected string"),
4372 "expected detail about wrong type, got: {}",
4373 errs[0]
4374 );
4375 }
4376
4377 #[test]
4380 fn test_match_pattern_string_against_int() {
4381 let warns = warnings(
4382 r#"pipeline t(task) {
4383 let x: int = 42
4384 match x {
4385 "hello" -> { log("bad") }
4386 42 -> { log("ok") }
4387 }
4388}"#,
4389 );
4390 let pattern_warns: Vec<_> = warns
4391 .iter()
4392 .filter(|w| w.contains("Match pattern type mismatch"))
4393 .collect();
4394 assert_eq!(pattern_warns.len(), 1);
4395 assert!(pattern_warns[0].contains("matching int against string literal"));
4396 }
4397
4398 #[test]
4399 fn test_match_pattern_int_against_string() {
4400 let warns = warnings(
4401 r#"pipeline t(task) {
4402 let x: string = "hello"
4403 match x {
4404 42 -> { log("bad") }
4405 "hello" -> { log("ok") }
4406 }
4407}"#,
4408 );
4409 let pattern_warns: Vec<_> = warns
4410 .iter()
4411 .filter(|w| w.contains("Match pattern type mismatch"))
4412 .collect();
4413 assert_eq!(pattern_warns.len(), 1);
4414 assert!(pattern_warns[0].contains("matching string against int literal"));
4415 }
4416
4417 #[test]
4418 fn test_match_pattern_bool_against_int() {
4419 let warns = warnings(
4420 r#"pipeline t(task) {
4421 let x: int = 42
4422 match x {
4423 true -> { log("bad") }
4424 42 -> { log("ok") }
4425 }
4426}"#,
4427 );
4428 let pattern_warns: Vec<_> = warns
4429 .iter()
4430 .filter(|w| w.contains("Match pattern type mismatch"))
4431 .collect();
4432 assert_eq!(pattern_warns.len(), 1);
4433 assert!(pattern_warns[0].contains("matching int against bool literal"));
4434 }
4435
4436 #[test]
4437 fn test_match_pattern_float_against_string() {
4438 let warns = warnings(
4439 r#"pipeline t(task) {
4440 let x: string = "hello"
4441 match x {
4442 3.14 -> { log("bad") }
4443 "hello" -> { log("ok") }
4444 }
4445}"#,
4446 );
4447 let pattern_warns: Vec<_> = warns
4448 .iter()
4449 .filter(|w| w.contains("Match pattern type mismatch"))
4450 .collect();
4451 assert_eq!(pattern_warns.len(), 1);
4452 assert!(pattern_warns[0].contains("matching string against float literal"));
4453 }
4454
4455 #[test]
4456 fn test_match_pattern_int_against_float_ok() {
4457 let warns = warnings(
4459 r#"pipeline t(task) {
4460 let x: float = 3.14
4461 match x {
4462 42 -> { log("ok") }
4463 _ -> { log("default") }
4464 }
4465}"#,
4466 );
4467 let pattern_warns: Vec<_> = warns
4468 .iter()
4469 .filter(|w| w.contains("Match pattern type mismatch"))
4470 .collect();
4471 assert!(pattern_warns.is_empty());
4472 }
4473
4474 #[test]
4475 fn test_match_pattern_float_against_int_ok() {
4476 let warns = warnings(
4478 r#"pipeline t(task) {
4479 let x: int = 42
4480 match x {
4481 3.14 -> { log("close") }
4482 _ -> { log("default") }
4483 }
4484}"#,
4485 );
4486 let pattern_warns: Vec<_> = warns
4487 .iter()
4488 .filter(|w| w.contains("Match pattern type mismatch"))
4489 .collect();
4490 assert!(pattern_warns.is_empty());
4491 }
4492
4493 #[test]
4494 fn test_match_pattern_correct_types_no_warning() {
4495 let warns = warnings(
4496 r#"pipeline t(task) {
4497 let x: int = 42
4498 match x {
4499 1 -> { log("one") }
4500 2 -> { log("two") }
4501 _ -> { log("other") }
4502 }
4503}"#,
4504 );
4505 let pattern_warns: Vec<_> = warns
4506 .iter()
4507 .filter(|w| w.contains("Match pattern type mismatch"))
4508 .collect();
4509 assert!(pattern_warns.is_empty());
4510 }
4511
4512 #[test]
4513 fn test_match_pattern_wildcard_no_warning() {
4514 let warns = warnings(
4515 r#"pipeline t(task) {
4516 let x: int = 42
4517 match x {
4518 _ -> { log("catch all") }
4519 }
4520}"#,
4521 );
4522 let pattern_warns: Vec<_> = warns
4523 .iter()
4524 .filter(|w| w.contains("Match pattern type mismatch"))
4525 .collect();
4526 assert!(pattern_warns.is_empty());
4527 }
4528
4529 #[test]
4530 fn test_match_pattern_untyped_no_warning() {
4531 let warns = warnings(
4533 r#"pipeline t(task) {
4534 let x = some_unknown_fn()
4535 match x {
4536 "hello" -> { log("string") }
4537 42 -> { log("int") }
4538 }
4539}"#,
4540 );
4541 let pattern_warns: Vec<_> = warns
4542 .iter()
4543 .filter(|w| w.contains("Match pattern type mismatch"))
4544 .collect();
4545 assert!(pattern_warns.is_empty());
4546 }
4547
4548 fn iface_errors(source: &str) -> Vec<String> {
4551 errors(source)
4552 .into_iter()
4553 .filter(|message| message.contains("does not satisfy interface"))
4554 .collect()
4555 }
4556
4557 #[test]
4558 fn test_interface_constraint_return_type_mismatch() {
4559 let warns = iface_errors(
4560 r#"pipeline t(task) {
4561 interface Sizable {
4562 fn size(self) -> int
4563 }
4564 struct Box { width: int }
4565 impl Box {
4566 fn size(self) -> string { return "nope" }
4567 }
4568 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4569 measure(Box({width: 3}))
4570}"#,
4571 );
4572 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4573 assert!(
4574 warns[0].contains("method 'size' returns 'string', expected 'int'"),
4575 "unexpected message: {}",
4576 warns[0]
4577 );
4578 }
4579
4580 #[test]
4581 fn test_interface_constraint_param_type_mismatch() {
4582 let warns = iface_errors(
4583 r#"pipeline t(task) {
4584 interface Processor {
4585 fn process(self, x: int) -> string
4586 }
4587 struct MyProc { name: string }
4588 impl MyProc {
4589 fn process(self, x: string) -> string { return x }
4590 }
4591 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
4592 run_proc(MyProc({name: "a"}))
4593}"#,
4594 );
4595 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4596 assert!(
4597 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
4598 "unexpected message: {}",
4599 warns[0]
4600 );
4601 }
4602
4603 #[test]
4604 fn test_interface_constraint_missing_method() {
4605 let warns = iface_errors(
4606 r#"pipeline t(task) {
4607 interface Sizable {
4608 fn size(self) -> int
4609 }
4610 struct Box { width: int }
4611 impl Box {
4612 fn area(self) -> int { return self.width }
4613 }
4614 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4615 measure(Box({width: 3}))
4616}"#,
4617 );
4618 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4619 assert!(
4620 warns[0].contains("missing method 'size'"),
4621 "unexpected message: {}",
4622 warns[0]
4623 );
4624 }
4625
4626 #[test]
4627 fn test_interface_constraint_param_count_mismatch() {
4628 let warns = iface_errors(
4629 r#"pipeline t(task) {
4630 interface Doubler {
4631 fn double(self, x: int) -> int
4632 }
4633 struct Bad { v: int }
4634 impl Bad {
4635 fn double(self) -> int { return self.v * 2 }
4636 }
4637 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
4638 run_double(Bad({v: 5}))
4639}"#,
4640 );
4641 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4642 assert!(
4643 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
4644 "unexpected message: {}",
4645 warns[0]
4646 );
4647 }
4648
4649 #[test]
4650 fn test_interface_constraint_satisfied() {
4651 let warns = iface_errors(
4652 r#"pipeline t(task) {
4653 interface Sizable {
4654 fn size(self) -> int
4655 }
4656 struct Box { width: int, height: int }
4657 impl Box {
4658 fn size(self) -> int { return self.width * self.height }
4659 }
4660 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4661 measure(Box({width: 3, height: 4}))
4662}"#,
4663 );
4664 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4665 }
4666
4667 #[test]
4668 fn test_interface_constraint_untyped_impl_compatible() {
4669 let warns = iface_errors(
4671 r#"pipeline t(task) {
4672 interface Sizable {
4673 fn size(self) -> int
4674 }
4675 struct Box { width: int }
4676 impl Box {
4677 fn size(self) { return self.width }
4678 }
4679 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4680 measure(Box({width: 3}))
4681}"#,
4682 );
4683 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4684 }
4685
4686 #[test]
4687 fn test_interface_constraint_int_float_covariance() {
4688 let warns = iface_errors(
4690 r#"pipeline t(task) {
4691 interface Measurable {
4692 fn value(self) -> float
4693 }
4694 struct Gauge { v: int }
4695 impl Gauge {
4696 fn value(self) -> int { return self.v }
4697 }
4698 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
4699 read_val(Gauge({v: 42}))
4700}"#,
4701 );
4702 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4703 }
4704
4705 #[test]
4706 fn test_interface_associated_type_constraint_satisfied() {
4707 let warns = iface_errors(
4708 r#"pipeline t(task) {
4709 interface Collection {
4710 type Item
4711 fn get(self, index: int) -> Item
4712 }
4713 struct Names {}
4714 impl Names {
4715 fn get(self, index: int) -> string { return "ada" }
4716 }
4717 fn first<C>(collection: C) where C: Collection {
4718 log(collection.get(0))
4719 }
4720 first(Names {})
4721}"#,
4722 );
4723 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4724 }
4725
4726 #[test]
4727 fn test_interface_associated_type_default_mismatch() {
4728 let warns = iface_errors(
4729 r#"pipeline t(task) {
4730 interface IntCollection {
4731 type Item = int
4732 fn get(self, index: int) -> Item
4733 }
4734 struct Labels {}
4735 impl Labels {
4736 fn get(self, index: int) -> string { return "oops" }
4737 }
4738 fn first<C>(collection: C) where C: IntCollection {
4739 log(collection.get(0))
4740 }
4741 first(Labels {})
4742}"#,
4743 );
4744 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4745 assert!(
4746 warns[0].contains("associated type 'Item' resolves to 'string', expected 'int'"),
4747 "unexpected message: {}",
4748 warns[0]
4749 );
4750 }
4751
4752 #[test]
4755 fn test_nil_narrowing_then_branch() {
4756 let errs = errors(
4758 r#"pipeline t(task) {
4759 fn greet(name: string | nil) {
4760 if name != nil {
4761 let s: string = name
4762 }
4763 }
4764}"#,
4765 );
4766 assert!(errs.is_empty(), "got: {:?}", errs);
4767 }
4768
4769 #[test]
4770 fn test_nil_narrowing_else_branch() {
4771 let errs = errors(
4773 r#"pipeline t(task) {
4774 fn check(x: string | nil) {
4775 if x != nil {
4776 let s: string = x
4777 } else {
4778 let n: nil = x
4779 }
4780 }
4781}"#,
4782 );
4783 assert!(errs.is_empty(), "got: {:?}", errs);
4784 }
4785
4786 #[test]
4787 fn test_nil_equality_narrows_both() {
4788 let errs = errors(
4790 r#"pipeline t(task) {
4791 fn check(x: string | nil) {
4792 if x == nil {
4793 let n: nil = x
4794 } else {
4795 let s: string = x
4796 }
4797 }
4798}"#,
4799 );
4800 assert!(errs.is_empty(), "got: {:?}", errs);
4801 }
4802
4803 #[test]
4804 fn test_truthiness_narrowing() {
4805 let errs = errors(
4807 r#"pipeline t(task) {
4808 fn check(x: string | nil) {
4809 if x {
4810 let s: string = x
4811 }
4812 }
4813}"#,
4814 );
4815 assert!(errs.is_empty(), "got: {:?}", errs);
4816 }
4817
4818 #[test]
4819 fn test_negation_narrowing() {
4820 let errs = errors(
4822 r#"pipeline t(task) {
4823 fn check(x: string | nil) {
4824 if !x {
4825 let n: nil = x
4826 } else {
4827 let s: string = x
4828 }
4829 }
4830}"#,
4831 );
4832 assert!(errs.is_empty(), "got: {:?}", errs);
4833 }
4834
4835 #[test]
4836 fn test_typeof_narrowing() {
4837 let errs = errors(
4839 r#"pipeline t(task) {
4840 fn check(x: string | int) {
4841 if type_of(x) == "string" {
4842 let s: string = x
4843 }
4844 }
4845}"#,
4846 );
4847 assert!(errs.is_empty(), "got: {:?}", errs);
4848 }
4849
4850 #[test]
4851 fn test_typeof_narrowing_else() {
4852 let errs = errors(
4854 r#"pipeline t(task) {
4855 fn check(x: string | int) {
4856 if type_of(x) == "string" {
4857 let s: string = x
4858 } else {
4859 let i: int = x
4860 }
4861 }
4862}"#,
4863 );
4864 assert!(errs.is_empty(), "got: {:?}", errs);
4865 }
4866
4867 #[test]
4868 fn test_typeof_neq_narrowing() {
4869 let errs = errors(
4871 r#"pipeline t(task) {
4872 fn check(x: string | int) {
4873 if type_of(x) != "string" {
4874 let i: int = x
4875 } else {
4876 let s: string = x
4877 }
4878 }
4879}"#,
4880 );
4881 assert!(errs.is_empty(), "got: {:?}", errs);
4882 }
4883
4884 #[test]
4885 fn test_and_combines_narrowing() {
4886 let errs = errors(
4888 r#"pipeline t(task) {
4889 fn check(x: string | int | nil) {
4890 if x != nil && type_of(x) == "string" {
4891 let s: string = x
4892 }
4893 }
4894}"#,
4895 );
4896 assert!(errs.is_empty(), "got: {:?}", errs);
4897 }
4898
4899 #[test]
4900 fn test_or_falsy_narrowing() {
4901 let errs = errors(
4903 r#"pipeline t(task) {
4904 fn check(x: string | nil, y: int | nil) {
4905 if x || y {
4906 // conservative: can't narrow
4907 } else {
4908 let xn: nil = x
4909 let yn: nil = y
4910 }
4911 }
4912}"#,
4913 );
4914 assert!(errs.is_empty(), "got: {:?}", errs);
4915 }
4916
4917 #[test]
4918 fn test_guard_narrows_outer_scope() {
4919 let errs = errors(
4920 r#"pipeline t(task) {
4921 fn check(x: string | nil) {
4922 guard x != nil else { return }
4923 let s: string = x
4924 }
4925}"#,
4926 );
4927 assert!(errs.is_empty(), "got: {:?}", errs);
4928 }
4929
4930 #[test]
4931 fn test_while_narrows_body() {
4932 let errs = errors(
4933 r#"pipeline t(task) {
4934 fn check(x: string | nil) {
4935 while x != nil {
4936 let s: string = x
4937 break
4938 }
4939 }
4940}"#,
4941 );
4942 assert!(errs.is_empty(), "got: {:?}", errs);
4943 }
4944
4945 #[test]
4946 fn test_early_return_narrows_after_if() {
4947 let errs = errors(
4949 r#"pipeline t(task) {
4950 fn check(x: string | nil) -> string {
4951 if x == nil {
4952 return "default"
4953 }
4954 let s: string = x
4955 return s
4956 }
4957}"#,
4958 );
4959 assert!(errs.is_empty(), "got: {:?}", errs);
4960 }
4961
4962 #[test]
4963 fn test_early_throw_narrows_after_if() {
4964 let errs = errors(
4965 r#"pipeline t(task) {
4966 fn check(x: string | nil) {
4967 if x == nil {
4968 throw "missing"
4969 }
4970 let s: string = x
4971 }
4972}"#,
4973 );
4974 assert!(errs.is_empty(), "got: {:?}", errs);
4975 }
4976
4977 #[test]
4978 fn test_no_narrowing_unknown_type() {
4979 let errs = errors(
4981 r#"pipeline t(task) {
4982 fn check(x) {
4983 if x != nil {
4984 let s: string = x
4985 }
4986 }
4987}"#,
4988 );
4989 assert!(errs.is_empty(), "got: {:?}", errs);
4992 }
4993
4994 #[test]
4995 fn test_reassignment_invalidates_narrowing() {
4996 let errs = errors(
4998 r#"pipeline t(task) {
4999 fn check(x: string | nil) {
5000 var y: string | nil = x
5001 if y != nil {
5002 let s: string = y
5003 y = nil
5004 let s2: string = y
5005 }
5006 }
5007}"#,
5008 );
5009 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
5011 assert!(
5012 errs[0].contains("Type mismatch"),
5013 "expected type mismatch, got: {}",
5014 errs[0]
5015 );
5016 }
5017
5018 #[test]
5019 fn test_let_immutable_warning() {
5020 let all = check_source(
5021 r#"pipeline t(task) {
5022 let x = 42
5023 x = 43
5024}"#,
5025 );
5026 let warnings: Vec<_> = all
5027 .iter()
5028 .filter(|d| d.severity == DiagnosticSeverity::Warning)
5029 .collect();
5030 assert!(
5031 warnings.iter().any(|w| w.message.contains("immutable")),
5032 "expected immutability warning, got: {:?}",
5033 warnings
5034 );
5035 }
5036
5037 #[test]
5038 fn test_nested_narrowing() {
5039 let errs = errors(
5040 r#"pipeline t(task) {
5041 fn check(x: string | int | nil) {
5042 if x != nil {
5043 if type_of(x) == "int" {
5044 let i: int = x
5045 }
5046 }
5047 }
5048}"#,
5049 );
5050 assert!(errs.is_empty(), "got: {:?}", errs);
5051 }
5052
5053 #[test]
5054 fn test_match_narrows_arms() {
5055 let errs = errors(
5056 r#"pipeline t(task) {
5057 fn check(x: string | int) {
5058 match x {
5059 "hello" -> {
5060 let s: string = x
5061 }
5062 42 -> {
5063 let i: int = x
5064 }
5065 _ -> {}
5066 }
5067 }
5068}"#,
5069 );
5070 assert!(errs.is_empty(), "got: {:?}", errs);
5071 }
5072
5073 #[test]
5074 fn test_has_narrows_optional_field() {
5075 let errs = errors(
5076 r#"pipeline t(task) {
5077 fn check(x: {name?: string, age: int}) {
5078 if x.has("name") {
5079 let n: {name: string, age: int} = x
5080 }
5081 }
5082}"#,
5083 );
5084 assert!(errs.is_empty(), "got: {:?}", errs);
5085 }
5086
5087 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
5092 let mut lexer = Lexer::new(source);
5093 let tokens = lexer.tokenize().unwrap();
5094 let mut parser = Parser::new(tokens);
5095 let program = parser.parse().unwrap();
5096 TypeChecker::new().check_with_source(&program, source)
5097 }
5098
5099 #[test]
5100 fn test_fix_string_plus_int_literal() {
5101 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
5102 let diags = check_source_with_source(source);
5103 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5104 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5105 let fix = fixable[0].fix.as_ref().unwrap();
5106 assert_eq!(fix.len(), 1);
5107 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
5108 }
5109
5110 #[test]
5111 fn test_fix_int_plus_string_literal() {
5112 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
5113 let diags = check_source_with_source(source);
5114 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5115 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5116 let fix = fixable[0].fix.as_ref().unwrap();
5117 assert_eq!(fix[0].replacement, "\"${42}hello\"");
5118 }
5119
5120 #[test]
5121 fn test_fix_string_plus_variable() {
5122 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
5123 let diags = check_source_with_source(source);
5124 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5125 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5126 let fix = fixable[0].fix.as_ref().unwrap();
5127 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
5128 }
5129
5130 #[test]
5131 fn test_no_fix_int_plus_int() {
5132 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}";
5134 let diags = check_source_with_source(source);
5135 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5136 assert!(
5137 fixable.is_empty(),
5138 "no fix expected for numeric ops, got: {fixable:?}"
5139 );
5140 }
5141
5142 #[test]
5143 fn test_no_fix_without_source() {
5144 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
5145 let diags = check_source(source);
5146 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5147 assert!(
5148 fixable.is_empty(),
5149 "without source, no fix should be generated"
5150 );
5151 }
5152
5153 #[test]
5156 fn test_union_exhaustive_match_no_warning() {
5157 let warns = warnings(
5158 r#"pipeline t(task) {
5159 let x: string | int | nil = nil
5160 match x {
5161 "hello" -> { log("s") }
5162 42 -> { log("i") }
5163 nil -> { log("n") }
5164 }
5165}"#,
5166 );
5167 let union_warns: Vec<_> = warns
5168 .iter()
5169 .filter(|w| w.contains("Non-exhaustive match on union"))
5170 .collect();
5171 assert!(union_warns.is_empty());
5172 }
5173
5174 #[test]
5175 fn test_union_non_exhaustive_match_warning() {
5176 let warns = warnings(
5177 r#"pipeline t(task) {
5178 let x: string | int | nil = nil
5179 match x {
5180 "hello" -> { log("s") }
5181 42 -> { log("i") }
5182 }
5183}"#,
5184 );
5185 let union_warns: Vec<_> = warns
5186 .iter()
5187 .filter(|w| w.contains("Non-exhaustive match on union"))
5188 .collect();
5189 assert_eq!(union_warns.len(), 1);
5190 assert!(union_warns[0].contains("nil"));
5191 }
5192
5193 #[test]
5196 fn test_nil_coalesce_non_union_preserves_left_type() {
5197 let errs = errors(
5199 r#"pipeline t(task) {
5200 let x: int = 42
5201 let y: int = x ?? 0
5202}"#,
5203 );
5204 assert!(errs.is_empty());
5205 }
5206
5207 #[test]
5208 fn test_nil_coalesce_nil_returns_right_type() {
5209 let errs = errors(
5210 r#"pipeline t(task) {
5211 let x: string = nil ?? "fallback"
5212}"#,
5213 );
5214 assert!(errs.is_empty());
5215 }
5216
5217 #[test]
5220 fn test_never_is_subtype_of_everything() {
5221 let tc = TypeChecker::new();
5222 let scope = TypeScope::new();
5223 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &TypeExpr::Never, &scope));
5224 assert!(tc.types_compatible(&TypeExpr::Named("int".into()), &TypeExpr::Never, &scope));
5225 assert!(tc.types_compatible(
5226 &TypeExpr::Union(vec![
5227 TypeExpr::Named("string".into()),
5228 TypeExpr::Named("nil".into()),
5229 ]),
5230 &TypeExpr::Never,
5231 &scope,
5232 ));
5233 }
5234
5235 #[test]
5236 fn test_nothing_is_subtype_of_never() {
5237 let tc = TypeChecker::new();
5238 let scope = TypeScope::new();
5239 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("string".into()), &scope));
5240 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("int".into()), &scope));
5241 }
5242
5243 #[test]
5244 fn test_never_never_compatible() {
5245 let tc = TypeChecker::new();
5246 let scope = TypeScope::new();
5247 assert!(tc.types_compatible(&TypeExpr::Never, &TypeExpr::Never, &scope));
5248 }
5249
5250 #[test]
5251 fn test_simplify_union_removes_never() {
5252 assert_eq!(
5253 simplify_union(vec![TypeExpr::Never, TypeExpr::Named("string".into())]),
5254 TypeExpr::Named("string".into()),
5255 );
5256 assert_eq!(
5257 simplify_union(vec![TypeExpr::Never, TypeExpr::Never]),
5258 TypeExpr::Never,
5259 );
5260 assert_eq!(
5261 simplify_union(vec![
5262 TypeExpr::Named("string".into()),
5263 TypeExpr::Never,
5264 TypeExpr::Named("int".into()),
5265 ]),
5266 TypeExpr::Union(vec![
5267 TypeExpr::Named("string".into()),
5268 TypeExpr::Named("int".into()),
5269 ]),
5270 );
5271 }
5272
5273 #[test]
5274 fn test_remove_from_union_exhausted_returns_never() {
5275 let result = remove_from_union(&[TypeExpr::Named("string".into())], "string");
5276 assert_eq!(result, Some(TypeExpr::Never));
5277 }
5278
5279 #[test]
5280 fn test_if_else_one_branch_throws_infers_other() {
5281 let errs = errors(
5283 r#"pipeline t(task) {
5284 fn foo(x: bool) -> int {
5285 let result: int = if x { 42 } else { throw "err" }
5286 return result
5287 }
5288}"#,
5289 );
5290 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5291 }
5292
5293 #[test]
5294 fn test_if_else_both_branches_throw_infers_never() {
5295 let errs = errors(
5297 r#"pipeline t(task) {
5298 fn foo(x: bool) -> string {
5299 let result: string = if x { throw "a" } else { throw "b" }
5300 return result
5301 }
5302}"#,
5303 );
5304 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5305 }
5306
5307 #[test]
5310 fn test_unreachable_after_return() {
5311 let warns = warnings(
5312 r#"pipeline t(task) {
5313 fn foo() -> int {
5314 return 1
5315 let x = 2
5316 }
5317}"#,
5318 );
5319 assert!(
5320 warns.iter().any(|w| w.contains("unreachable")),
5321 "expected unreachable warning: {warns:?}"
5322 );
5323 }
5324
5325 #[test]
5326 fn test_unreachable_after_throw() {
5327 let warns = warnings(
5328 r#"pipeline t(task) {
5329 fn foo() {
5330 throw "err"
5331 let x = 2
5332 }
5333}"#,
5334 );
5335 assert!(
5336 warns.iter().any(|w| w.contains("unreachable")),
5337 "expected unreachable warning: {warns:?}"
5338 );
5339 }
5340
5341 #[test]
5342 fn test_unreachable_after_composite_exit() {
5343 let warns = warnings(
5344 r#"pipeline t(task) {
5345 fn foo(x: bool) {
5346 if x { return 1 } else { throw "err" }
5347 let y = 2
5348 }
5349}"#,
5350 );
5351 assert!(
5352 warns.iter().any(|w| w.contains("unreachable")),
5353 "expected unreachable warning: {warns:?}"
5354 );
5355 }
5356
5357 #[test]
5358 fn test_no_unreachable_warning_when_reachable() {
5359 let warns = warnings(
5360 r#"pipeline t(task) {
5361 fn foo(x: bool) {
5362 if x { return 1 }
5363 let y = 2
5364 }
5365}"#,
5366 );
5367 assert!(
5368 !warns.iter().any(|w| w.contains("unreachable")),
5369 "unexpected unreachable warning: {warns:?}"
5370 );
5371 }
5372
5373 #[test]
5376 fn test_catch_typed_error_variable() {
5377 let errs = errors(
5379 r#"pipeline t(task) {
5380 enum AppError { NotFound, Timeout }
5381 try {
5382 throw AppError.NotFound
5383 } catch (e: AppError) {
5384 let x: AppError = e
5385 }
5386}"#,
5387 );
5388 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5389 }
5390
5391 #[test]
5394 fn test_unreachable_with_never_arg_no_error() {
5395 let errs = errors(
5397 r#"pipeline t(task) {
5398 fn foo(x: string | int) {
5399 if type_of(x) == "string" { return }
5400 if type_of(x) == "int" { return }
5401 unreachable(x)
5402 }
5403}"#,
5404 );
5405 assert!(
5406 !errs.iter().any(|e| e.contains("unreachable")),
5407 "unexpected unreachable error: {errs:?}"
5408 );
5409 }
5410
5411 #[test]
5412 fn test_unreachable_with_remaining_types_errors() {
5413 let errs = errors(
5415 r#"pipeline t(task) {
5416 fn foo(x: string | int | nil) {
5417 if type_of(x) == "string" { return }
5418 unreachable(x)
5419 }
5420}"#,
5421 );
5422 assert!(
5423 errs.iter()
5424 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
5425 "expected unreachable error about remaining types: {errs:?}"
5426 );
5427 }
5428
5429 #[test]
5430 fn test_unreachable_no_args_no_compile_error() {
5431 let errs = errors(
5432 r#"pipeline t(task) {
5433 fn foo() {
5434 unreachable()
5435 }
5436}"#,
5437 );
5438 assert!(
5439 !errs
5440 .iter()
5441 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
5442 "unreachable() with no args should not produce type error: {errs:?}"
5443 );
5444 }
5445
5446 #[test]
5447 fn test_never_type_annotation_parses() {
5448 let errs = errors(
5449 r#"pipeline t(task) {
5450 fn foo() -> never {
5451 throw "always throws"
5452 }
5453}"#,
5454 );
5455 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5456 }
5457
5458 #[test]
5459 fn test_format_type_never() {
5460 assert_eq!(format_type(&TypeExpr::Never), "never");
5461 }
5462}