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