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, Copy, PartialEq, Eq)]
42enum Polarity {
43 Covariant,
44 Contravariant,
45 Invariant,
46}
47
48impl Polarity {
49 fn compose(self, child: Variance) -> Polarity {
53 match (self, child) {
54 (_, Variance::Invariant) | (Polarity::Invariant, _) => Polarity::Invariant,
55 (p, Variance::Covariant) => p,
56 (Polarity::Covariant, Variance::Contravariant) => Polarity::Contravariant,
57 (Polarity::Contravariant, Variance::Contravariant) => Polarity::Covariant,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
63struct EnumDeclInfo {
64 type_params: Vec<TypeParam>,
65 variants: Vec<EnumVariant>,
66}
67
68#[derive(Debug, Clone)]
69struct StructDeclInfo {
70 type_params: Vec<TypeParam>,
71 fields: Vec<StructField>,
72}
73
74#[derive(Debug, Clone)]
75struct InterfaceDeclInfo {
76 type_params: Vec<TypeParam>,
77 associated_types: Vec<(String, Option<TypeExpr>)>,
78 methods: Vec<InterfaceMethod>,
79}
80
81#[derive(Debug, Clone)]
83struct TypeScope {
84 vars: BTreeMap<String, InferredType>,
86 functions: BTreeMap<String, FnSignature>,
88 type_aliases: BTreeMap<String, TypeExpr>,
90 enums: BTreeMap<String, EnumDeclInfo>,
92 interfaces: BTreeMap<String, InterfaceDeclInfo>,
94 structs: BTreeMap<String, StructDeclInfo>,
96 impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
98 generic_type_params: std::collections::BTreeSet<String>,
100 where_constraints: BTreeMap<String, String>,
103 mutable_vars: std::collections::BTreeSet<String>,
106 narrowed_vars: BTreeMap<String, InferredType>,
109 schema_bindings: BTreeMap<String, InferredType>,
112 untyped_sources: BTreeMap<String, String>,
116 unknown_ruled_out: BTreeMap<String, Vec<String>>,
120 parent: Option<Box<TypeScope>>,
121}
122
123#[derive(Debug, Clone)]
125struct ImplMethodSig {
126 name: String,
127 param_count: usize,
129 param_types: Vec<Option<TypeExpr>>,
131 return_type: Option<TypeExpr>,
133}
134
135#[derive(Debug, Clone)]
136struct FnSignature {
137 params: Vec<(String, InferredType)>,
138 return_type: InferredType,
139 type_param_names: Vec<String>,
141 required_params: usize,
143 where_clauses: Vec<(String, String)>,
145 has_rest: bool,
147}
148
149impl TypeScope {
150 fn new() -> Self {
151 let mut scope = Self {
152 vars: BTreeMap::new(),
153 functions: BTreeMap::new(),
154 type_aliases: BTreeMap::new(),
155 enums: BTreeMap::new(),
156 interfaces: BTreeMap::new(),
157 structs: BTreeMap::new(),
158 impl_methods: BTreeMap::new(),
159 generic_type_params: std::collections::BTreeSet::new(),
160 where_constraints: BTreeMap::new(),
161 mutable_vars: std::collections::BTreeSet::new(),
162 narrowed_vars: BTreeMap::new(),
163 schema_bindings: BTreeMap::new(),
164 untyped_sources: BTreeMap::new(),
165 unknown_ruled_out: BTreeMap::new(),
166 parent: None,
167 };
168 scope.enums.insert(
169 "Result".into(),
170 EnumDeclInfo {
171 type_params: vec![
172 TypeParam {
173 name: "T".into(),
174 variance: Variance::Covariant,
175 },
176 TypeParam {
177 name: "E".into(),
178 variance: Variance::Covariant,
179 },
180 ],
181 variants: vec![
182 EnumVariant {
183 name: "Ok".into(),
184 fields: vec![TypedParam {
185 name: "value".into(),
186 type_expr: Some(TypeExpr::Named("T".into())),
187 default_value: None,
188 rest: false,
189 }],
190 },
191 EnumVariant {
192 name: "Err".into(),
193 fields: vec![TypedParam {
194 name: "error".into(),
195 type_expr: Some(TypeExpr::Named("E".into())),
196 default_value: None,
197 rest: false,
198 }],
199 },
200 ],
201 },
202 );
203 scope
204 }
205
206 fn child(&self) -> Self {
207 Self {
208 vars: BTreeMap::new(),
209 functions: BTreeMap::new(),
210 type_aliases: BTreeMap::new(),
211 enums: BTreeMap::new(),
212 interfaces: BTreeMap::new(),
213 structs: BTreeMap::new(),
214 impl_methods: BTreeMap::new(),
215 generic_type_params: std::collections::BTreeSet::new(),
216 where_constraints: BTreeMap::new(),
217 mutable_vars: std::collections::BTreeSet::new(),
218 narrowed_vars: BTreeMap::new(),
219 schema_bindings: BTreeMap::new(),
220 untyped_sources: BTreeMap::new(),
221 unknown_ruled_out: BTreeMap::new(),
222 parent: Some(Box::new(self.clone())),
223 }
224 }
225
226 fn get_var(&self, name: &str) -> Option<&InferredType> {
227 self.vars
228 .get(name)
229 .or_else(|| self.parent.as_ref()?.get_var(name))
230 }
231
232 fn add_unknown_ruled_out(&mut self, var_name: &str, type_name: &str) {
235 if !self.unknown_ruled_out.contains_key(var_name) {
236 let inherited = self.lookup_unknown_ruled_out(var_name);
237 self.unknown_ruled_out
238 .insert(var_name.to_string(), inherited);
239 }
240 let entry = self
241 .unknown_ruled_out
242 .get_mut(var_name)
243 .expect("just inserted");
244 if !entry.iter().any(|t| t == type_name) {
245 entry.push(type_name.to_string());
246 }
247 }
248
249 fn lookup_unknown_ruled_out(&self, var_name: &str) -> Vec<String> {
252 if let Some(list) = self.unknown_ruled_out.get(var_name) {
253 list.clone()
254 } else if let Some(parent) = &self.parent {
255 parent.lookup_unknown_ruled_out(var_name)
256 } else {
257 Vec::new()
258 }
259 }
260
261 fn collect_unknown_ruled_out(&self) -> BTreeMap<String, Vec<String>> {
264 let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
265 self.collect_unknown_ruled_out_inner(&mut out);
266 out
267 }
268
269 fn collect_unknown_ruled_out_inner(&self, acc: &mut BTreeMap<String, Vec<String>>) {
270 if let Some(parent) = &self.parent {
271 parent.collect_unknown_ruled_out_inner(acc);
272 }
273 for (name, list) in &self.unknown_ruled_out {
274 acc.insert(name.clone(), list.clone());
275 }
276 }
277
278 fn clear_unknown_ruled_out(&mut self, var_name: &str) {
280 self.unknown_ruled_out
283 .insert(var_name.to_string(), Vec::new());
284 }
285
286 fn get_fn(&self, name: &str) -> Option<&FnSignature> {
287 self.functions
288 .get(name)
289 .or_else(|| self.parent.as_ref()?.get_fn(name))
290 }
291
292 fn get_schema_binding(&self, name: &str) -> Option<&InferredType> {
293 self.schema_bindings
294 .get(name)
295 .or_else(|| self.parent.as_ref()?.get_schema_binding(name))
296 }
297
298 fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
299 self.type_aliases
300 .get(name)
301 .or_else(|| self.parent.as_ref()?.resolve_type(name))
302 }
303
304 fn is_generic_type_param(&self, name: &str) -> bool {
305 self.generic_type_params.contains(name)
306 || self
307 .parent
308 .as_ref()
309 .is_some_and(|p| p.is_generic_type_param(name))
310 }
311
312 fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
313 self.where_constraints
314 .get(type_param)
315 .map(|s| s.as_str())
316 .or_else(|| {
317 self.parent
318 .as_ref()
319 .and_then(|p| p.get_where_constraint(type_param))
320 })
321 }
322
323 fn get_enum(&self, name: &str) -> Option<&EnumDeclInfo> {
324 self.enums
325 .get(name)
326 .or_else(|| self.parent.as_ref()?.get_enum(name))
327 }
328
329 fn get_interface(&self, name: &str) -> Option<&InterfaceDeclInfo> {
330 self.interfaces
331 .get(name)
332 .or_else(|| self.parent.as_ref()?.get_interface(name))
333 }
334
335 fn get_struct(&self, name: &str) -> Option<&StructDeclInfo> {
336 self.structs
337 .get(name)
338 .or_else(|| self.parent.as_ref()?.get_struct(name))
339 }
340
341 fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
342 self.impl_methods
343 .get(name)
344 .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
345 }
346
347 fn variance_of(&self, name: &str) -> Option<Vec<Variance>> {
354 if let Some(info) = self.get_enum(name) {
355 return Some(info.type_params.iter().map(|tp| tp.variance).collect());
356 }
357 if let Some(info) = self.get_struct(name) {
358 return Some(info.type_params.iter().map(|tp| tp.variance).collect());
359 }
360 if let Some(info) = self.get_interface(name) {
361 return Some(info.type_params.iter().map(|tp| tp.variance).collect());
362 }
363 None
364 }
365
366 fn define_var(&mut self, name: &str, ty: InferredType) {
367 self.vars.insert(name.to_string(), ty);
368 }
369
370 fn define_var_mutable(&mut self, name: &str, ty: InferredType) {
371 self.vars.insert(name.to_string(), ty);
372 self.mutable_vars.insert(name.to_string());
373 }
374
375 fn define_schema_binding(&mut self, name: &str, ty: InferredType) {
376 self.schema_bindings.insert(name.to_string(), ty);
377 }
378
379 fn is_untyped_source(&self, name: &str) -> Option<&str> {
382 if let Some(source) = self.untyped_sources.get(name) {
383 if source.is_empty() {
384 return None; }
386 return Some(source.as_str());
387 }
388 self.parent.as_ref()?.is_untyped_source(name)
389 }
390
391 fn mark_untyped_source(&mut self, name: &str, source: &str) {
392 self.untyped_sources
393 .insert(name.to_string(), source.to_string());
394 }
395
396 fn clear_untyped_source(&mut self, name: &str) {
398 self.untyped_sources.insert(name.to_string(), String::new());
399 }
400
401 fn is_mutable(&self, name: &str) -> bool {
403 self.mutable_vars.contains(name) || self.parent.as_ref().is_some_and(|p| p.is_mutable(name))
404 }
405
406 fn define_fn(&mut self, name: &str, sig: FnSignature) {
407 self.functions.insert(name.to_string(), sig);
408 }
409}
410
411#[derive(Debug, Clone, Default)]
414struct Refinements {
415 truthy: Vec<(String, InferredType)>,
417 falsy: Vec<(String, InferredType)>,
419 truthy_ruled_out: Vec<(String, String)>,
423 falsy_ruled_out: Vec<(String, String)>,
427}
428
429impl Refinements {
430 fn empty() -> Self {
431 Self::default()
432 }
433
434 fn inverted(self) -> Self {
436 Self {
437 truthy: self.falsy,
438 falsy: self.truthy,
439 truthy_ruled_out: self.falsy_ruled_out,
440 falsy_ruled_out: self.truthy_ruled_out,
441 }
442 }
443
444 fn apply_truthy(&self, scope: &mut TypeScope) {
446 apply_refinements(scope, &self.truthy);
447 for (var, ty) in &self.truthy_ruled_out {
448 scope.add_unknown_ruled_out(var, ty);
449 }
450 }
451
452 fn apply_falsy(&self, scope: &mut TypeScope) {
454 apply_refinements(scope, &self.falsy);
455 for (var, ty) in &self.falsy_ruled_out {
456 scope.add_unknown_ruled_out(var, ty);
457 }
458 }
459}
460
461fn builtin_return_type(name: &str) -> InferredType {
464 builtin_signatures::builtin_return_type(name)
465}
466
467fn is_builtin(name: &str) -> bool {
470 builtin_signatures::is_builtin(name)
471}
472
473pub struct TypeChecker {
475 diagnostics: Vec<TypeDiagnostic>,
476 scope: TypeScope,
477 source: Option<String>,
478 hints: Vec<InlayHintInfo>,
479 strict_types: bool,
481 fn_depth: usize,
484 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
489}
490
491impl TypeChecker {
492 fn wildcard_type() -> TypeExpr {
493 TypeExpr::Named("_".into())
494 }
495
496 fn is_wildcard_type(ty: &TypeExpr) -> bool {
497 matches!(ty, TypeExpr::Named(name) if name == "_")
498 }
499
500 fn base_type_name(ty: &TypeExpr) -> Option<&str> {
501 match ty {
502 TypeExpr::Named(name) => Some(name.as_str()),
503 TypeExpr::Applied { name, .. } => Some(name.as_str()),
504 _ => None,
505 }
506 }
507
508 pub fn new() -> Self {
509 Self {
510 diagnostics: Vec::new(),
511 scope: TypeScope::new(),
512 source: None,
513 hints: Vec::new(),
514 strict_types: false,
515 fn_depth: 0,
516 deprecated_fns: std::collections::HashMap::new(),
517 }
518 }
519
520 pub fn with_strict_types(strict: bool) -> Self {
523 Self {
524 diagnostics: Vec::new(),
525 scope: TypeScope::new(),
526 source: None,
527 hints: Vec::new(),
528 strict_types: strict,
529 fn_depth: 0,
530 deprecated_fns: std::collections::HashMap::new(),
531 }
532 }
533
534 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
536 self.source = Some(source.to_string());
537 self.check_inner(program).0
538 }
539
540 pub fn check_strict_with_source(
542 mut self,
543 program: &[SNode],
544 source: &str,
545 ) -> Vec<TypeDiagnostic> {
546 self.source = Some(source.to_string());
547 self.check_inner(program).0
548 }
549
550 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
552 self.check_inner(program).0
553 }
554
555 fn detect_boundary_source(value: &SNode, scope: &TypeScope) -> Option<String> {
559 match &value.node {
560 Node::FunctionCall { name, args } => {
561 if !builtin_signatures::is_untyped_boundary_source(name) {
562 return None;
563 }
564 if (name == "llm_call" || name == "llm_completion")
566 && Self::llm_call_has_typed_schema_option(args, scope)
567 {
568 return None;
569 }
570 Some(name.clone())
571 }
572 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
573 _ => None,
574 }
575 }
576
577 fn llm_call_has_typed_schema_option(args: &[SNode], scope: &TypeScope) -> bool {
583 let Some(opts) = args.get(2) else {
584 return false;
585 };
586 let Node::DictLiteral(entries) = &opts.node else {
587 return false;
588 };
589 entries.iter().any(|entry| {
590 let key = match &entry.key.node {
591 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
592 _ => return false,
593 };
594 (key == "schema" || key == "output_schema")
595 && schema_type_expr_from_node(&entry.value, scope).is_some()
596 })
597 }
598
599 fn is_concrete_type(ty: &TypeExpr) -> bool {
602 matches!(
603 ty,
604 TypeExpr::Shape(_)
605 | TypeExpr::Applied { .. }
606 | TypeExpr::FnType { .. }
607 | TypeExpr::List(_)
608 | TypeExpr::Iter(_)
609 | TypeExpr::DictType(_, _)
610 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
611 }
612
613 pub fn check_with_hints(
615 mut self,
616 program: &[SNode],
617 source: &str,
618 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
619 self.source = Some(source.to_string());
620 self.check_inner(program)
621 }
622
623 fn check_inner(mut self, program: &[SNode]) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
624 Self::register_declarations_into(&mut self.scope, program);
627 for snode in program {
628 if let Node::Pipeline { body, .. } = &snode.node {
629 Self::register_declarations_into(&mut self.scope, body);
630 }
631 }
632
633 for snode in program {
637 if let Node::AttributedDecl { attributes, inner } = &snode.node {
638 if let Node::FnDecl { name, .. } = &inner.node {
639 for attr in attributes {
640 if attr.name == "deprecated" {
641 let since = attr.string_arg("since");
642 let use_hint = attr.string_arg("use");
643 self.deprecated_fns.insert(name.clone(), (since, use_hint));
644 }
645 }
646 }
647 }
648 }
649
650 if !self.deprecated_fns.is_empty() {
655 for snode in program {
656 self.visit_for_deprecation(snode);
657 }
658 }
659
660 for snode in program {
661 let inner_node = match &snode.node {
666 Node::AttributedDecl { inner, .. } => inner.as_ref(),
667 _ => snode,
668 };
669 match &inner_node.node {
670 Node::Pipeline { params, body, .. } => {
671 let mut child = self.scope.child();
672 for p in params {
673 child.define_var(p, None);
674 }
675 self.fn_depth += 1;
676 self.check_block(body, &mut child);
677 self.fn_depth -= 1;
678 }
679 Node::FnDecl {
680 name,
681 type_params,
682 params,
683 return_type,
684 where_clauses,
685 body,
686 ..
687 } => {
688 let required_params =
689 params.iter().filter(|p| p.default_value.is_none()).count();
690 let sig = FnSignature {
691 params: params
692 .iter()
693 .map(|p| (p.name.clone(), p.type_expr.clone()))
694 .collect(),
695 return_type: return_type.clone(),
696 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
697 required_params,
698 where_clauses: where_clauses
699 .iter()
700 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
701 .collect(),
702 has_rest: params.last().is_some_and(|p| p.rest),
703 };
704 self.scope.define_fn(name, sig);
705 self.check_fn_body(type_params, params, return_type, body, where_clauses);
706 }
707 _ => {
708 let mut scope = self.scope.clone();
709 self.check_node(snode, &mut scope);
710 for (name, ty) in scope.vars {
712 self.scope.vars.entry(name).or_insert(ty);
713 }
714 for name in scope.mutable_vars {
715 self.scope.mutable_vars.insert(name);
716 }
717 }
718 }
719 }
720
721 (self.diagnostics, self.hints)
722 }
723
724 fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
726 for snode in nodes {
727 match &snode.node {
728 Node::TypeDecl {
729 name,
730 type_params: _,
731 type_expr,
732 } => {
733 scope.type_aliases.insert(name.clone(), type_expr.clone());
734 }
735 Node::EnumDecl {
736 name,
737 type_params,
738 variants,
739 ..
740 } => {
741 scope.enums.insert(
742 name.clone(),
743 EnumDeclInfo {
744 type_params: type_params.clone(),
745 variants: variants.clone(),
746 },
747 );
748 }
749 Node::InterfaceDecl {
750 name,
751 type_params,
752 associated_types,
753 methods,
754 } => {
755 scope.interfaces.insert(
756 name.clone(),
757 InterfaceDeclInfo {
758 type_params: type_params.clone(),
759 associated_types: associated_types.clone(),
760 methods: methods.clone(),
761 },
762 );
763 }
764 Node::StructDecl {
765 name,
766 type_params,
767 fields,
768 ..
769 } => {
770 scope.structs.insert(
771 name.clone(),
772 StructDeclInfo {
773 type_params: type_params.clone(),
774 fields: fields.clone(),
775 },
776 );
777 }
778 Node::ImplBlock {
779 type_name, methods, ..
780 } => {
781 let sigs: Vec<ImplMethodSig> = methods
782 .iter()
783 .filter_map(|m| {
784 if let Node::FnDecl {
785 name,
786 params,
787 return_type,
788 ..
789 } = &m.node
790 {
791 let non_self: Vec<_> =
792 params.iter().filter(|p| p.name != "self").collect();
793 let param_count = non_self.len();
794 let param_types: Vec<Option<TypeExpr>> =
795 non_self.iter().map(|p| p.type_expr.clone()).collect();
796 Some(ImplMethodSig {
797 name: name.clone(),
798 param_count,
799 param_types,
800 return_type: return_type.clone(),
801 })
802 } else {
803 None
804 }
805 })
806 .collect();
807 scope.impl_methods.insert(type_name.clone(), sigs);
808 }
809 _ => {}
810 }
811 }
812 }
813
814 fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
815 let mut definitely_exited = false;
816 for stmt in stmts {
817 if definitely_exited {
818 self.warning_at("unreachable code".to_string(), stmt.span);
819 break; }
821 self.check_node(stmt, scope);
822 if Self::stmt_definitely_exits(stmt) {
823 definitely_exited = true;
824 }
825 }
826 }
827
828 fn stmt_definitely_exits(stmt: &SNode) -> bool {
830 stmt_definitely_exits(stmt)
831 }
832
833 fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope, mutable: bool) {
835 let define = |scope: &mut TypeScope, name: &str| {
836 if mutable {
837 scope.define_var_mutable(name, None);
838 } else {
839 scope.define_var(name, None);
840 }
841 };
842 match pattern {
843 BindingPattern::Identifier(name) => {
844 define(scope, name);
845 }
846 BindingPattern::Dict(fields) => {
847 for field in fields {
848 let name = field.alias.as_deref().unwrap_or(&field.key);
849 define(scope, name);
850 }
851 }
852 BindingPattern::List(elements) => {
853 for elem in elements {
854 define(scope, &elem.name);
855 }
856 }
857 BindingPattern::Pair(a, b) => {
858 define(scope, a);
859 define(scope, b);
860 }
861 }
862 }
863
864 fn check_pattern_defaults(&mut self, pattern: &BindingPattern, scope: &mut TypeScope) {
866 match pattern {
867 BindingPattern::Identifier(_) => {}
868 BindingPattern::Dict(fields) => {
869 for field in fields {
870 if let Some(default) = &field.default_value {
871 self.check_binops(default, scope);
872 }
873 }
874 }
875 BindingPattern::List(elements) => {
876 for elem in elements {
877 if let Some(default) = &elem.default_value {
878 self.check_binops(default, scope);
879 }
880 }
881 }
882 BindingPattern::Pair(_, _) => {}
883 }
884 }
885
886 fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
887 let span = snode.span;
888 match &snode.node {
889 Node::LetBinding {
890 pattern,
891 type_ann,
892 value,
893 } => {
894 self.check_binops(value, scope);
895 let inferred = self.infer_type(value, scope);
896 if let BindingPattern::Identifier(name) = pattern {
897 if let Some(expected) = type_ann {
898 if let Some(actual) = &inferred {
899 if !self.types_compatible(expected, actual, scope) {
900 let mut msg = format!(
901 "'{}' declared as {}, but assigned {}",
902 name,
903 format_type(expected),
904 format_type(actual)
905 );
906 if let Some(detail) = shape_mismatch_detail(expected, actual) {
907 msg.push_str(&format!(" ({})", detail));
908 }
909 self.error_at(msg, span);
910 }
911 }
912 }
913 if type_ann.is_none() {
915 if let Some(ref ty) = inferred {
916 if !is_obvious_type(value, ty) {
917 self.hints.push(InlayHintInfo {
918 line: span.line,
919 column: span.column + "let ".len() + name.len(),
920 label: format!(": {}", format_type(ty)),
921 });
922 }
923 }
924 }
925 let ty = type_ann.clone().or(inferred);
926 scope.define_var(name, ty);
927 scope.define_schema_binding(name, schema_type_expr_from_node(value, scope));
928 if self.strict_types {
930 if let Some(boundary) = Self::detect_boundary_source(value, scope) {
931 let has_concrete_ann =
932 type_ann.as_ref().is_some_and(Self::is_concrete_type);
933 if !has_concrete_ann {
934 scope.mark_untyped_source(name, &boundary);
935 }
936 }
937 }
938 } else {
939 self.check_pattern_defaults(pattern, scope);
940 Self::define_pattern_vars(pattern, scope, false);
941 }
942 }
943
944 Node::VarBinding {
945 pattern,
946 type_ann,
947 value,
948 } => {
949 self.check_binops(value, scope);
950 let inferred = self.infer_type(value, scope);
951 if let BindingPattern::Identifier(name) = pattern {
952 if let Some(expected) = type_ann {
953 if let Some(actual) = &inferred {
954 if !self.types_compatible(expected, actual, scope) {
955 let mut msg = format!(
956 "'{}' declared as {}, but assigned {}",
957 name,
958 format_type(expected),
959 format_type(actual)
960 );
961 if let Some(detail) = shape_mismatch_detail(expected, actual) {
962 msg.push_str(&format!(" ({})", detail));
963 }
964 self.error_at(msg, span);
965 }
966 }
967 }
968 if type_ann.is_none() {
969 if let Some(ref ty) = inferred {
970 if !is_obvious_type(value, ty) {
971 self.hints.push(InlayHintInfo {
972 line: span.line,
973 column: span.column + "var ".len() + name.len(),
974 label: format!(": {}", format_type(ty)),
975 });
976 }
977 }
978 }
979 let ty = type_ann.clone().or(inferred);
980 scope.define_var_mutable(name, ty);
981 scope.define_schema_binding(name, schema_type_expr_from_node(value, scope));
982 if self.strict_types {
984 if let Some(boundary) = Self::detect_boundary_source(value, scope) {
985 let has_concrete_ann =
986 type_ann.as_ref().is_some_and(Self::is_concrete_type);
987 if !has_concrete_ann {
988 scope.mark_untyped_source(name, &boundary);
989 }
990 }
991 }
992 } else {
993 self.check_pattern_defaults(pattern, scope);
994 Self::define_pattern_vars(pattern, scope, true);
995 }
996 }
997
998 Node::FnDecl {
999 name,
1000 type_params,
1001 params,
1002 return_type,
1003 where_clauses,
1004 body,
1005 ..
1006 } => {
1007 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
1008 let sig = FnSignature {
1009 params: params
1010 .iter()
1011 .map(|p| (p.name.clone(), p.type_expr.clone()))
1012 .collect(),
1013 return_type: return_type.clone(),
1014 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
1015 required_params,
1016 where_clauses: where_clauses
1017 .iter()
1018 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
1019 .collect(),
1020 has_rest: params.last().is_some_and(|p| p.rest),
1021 };
1022 scope.define_fn(name, sig.clone());
1023 scope.define_var(name, None);
1024 self.check_fn_decl_variance(type_params, params, return_type.as_ref(), name, span);
1025 self.check_fn_body(type_params, params, return_type, body, where_clauses);
1026 }
1027
1028 Node::ToolDecl {
1029 name,
1030 params,
1031 return_type,
1032 body,
1033 ..
1034 } => {
1035 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
1037 let sig = FnSignature {
1038 params: params
1039 .iter()
1040 .map(|p| (p.name.clone(), p.type_expr.clone()))
1041 .collect(),
1042 return_type: return_type.clone(),
1043 type_param_names: Vec::new(),
1044 required_params,
1045 where_clauses: Vec::new(),
1046 has_rest: params.last().is_some_and(|p| p.rest),
1047 };
1048 scope.define_fn(name, sig);
1049 scope.define_var(name, None);
1050 self.check_fn_body(&[], params, return_type, body, &[]);
1051 }
1052
1053 Node::FunctionCall { name, args } => {
1054 self.check_call(name, args, scope, span);
1055 if self.strict_types && name == "schema_expect" && args.len() >= 2 {
1057 if let Node::Identifier(var_name) = &args[0].node {
1058 scope.clear_untyped_source(var_name);
1059 if let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) {
1060 scope.define_var(var_name, Some(schema_type));
1061 }
1062 }
1063 }
1064 }
1065
1066 Node::IfElse {
1067 condition,
1068 then_body,
1069 else_body,
1070 } => {
1071 self.check_node(condition, scope);
1072 let refs = Self::extract_refinements(condition, scope);
1073
1074 let mut then_scope = scope.child();
1075 refs.apply_truthy(&mut then_scope);
1076 if self.strict_types {
1079 if let Node::FunctionCall { name, args } = &condition.node {
1080 if (name == "schema_is" || name == "is_type") && args.len() == 2 {
1081 if let Node::Identifier(var_name) = &args[0].node {
1082 then_scope.clear_untyped_source(var_name);
1083 }
1084 }
1085 }
1086 }
1087 self.check_block(then_body, &mut then_scope);
1088
1089 if let Some(else_body) = else_body {
1090 let mut else_scope = scope.child();
1091 refs.apply_falsy(&mut else_scope);
1092 self.check_block(else_body, &mut else_scope);
1093
1094 if Self::block_definitely_exits(then_body)
1097 && !Self::block_definitely_exits(else_body)
1098 {
1099 refs.apply_falsy(scope);
1100 } else if Self::block_definitely_exits(else_body)
1101 && !Self::block_definitely_exits(then_body)
1102 {
1103 refs.apply_truthy(scope);
1104 }
1105 } else {
1106 if Self::block_definitely_exits(then_body) {
1108 refs.apply_falsy(scope);
1109 }
1110 }
1111 }
1112
1113 Node::ForIn {
1114 pattern,
1115 iterable,
1116 body,
1117 } => {
1118 self.check_node(iterable, scope);
1119 let mut loop_scope = scope.child();
1120 let iter_type = self.infer_type(iterable, scope);
1121 if let BindingPattern::Identifier(variable) = pattern {
1122 let elem_type = match iter_type {
1124 Some(TypeExpr::List(inner)) => Some(*inner),
1125 Some(TypeExpr::Iter(inner)) => Some(*inner),
1126 Some(TypeExpr::Applied { ref name, ref args })
1127 if name == "Iter" && args.len() == 1 =>
1128 {
1129 Some(args[0].clone())
1130 }
1131 Some(TypeExpr::Named(n)) if n == "string" => {
1132 Some(TypeExpr::Named("string".into()))
1133 }
1134 Some(TypeExpr::Named(n)) if n == "range" => {
1136 Some(TypeExpr::Named("int".into()))
1137 }
1138 _ => None,
1139 };
1140 loop_scope.define_var(variable, elem_type);
1141 } else if let BindingPattern::Pair(a, b) = pattern {
1142 let (ka, vb) = match &iter_type {
1145 Some(TypeExpr::Iter(inner)) => {
1146 if let TypeExpr::Applied { name, args } = inner.as_ref() {
1147 if name == "Pair" && args.len() == 2 {
1148 (Some(args[0].clone()), Some(args[1].clone()))
1149 } else {
1150 (None, None)
1151 }
1152 } else {
1153 (None, None)
1154 }
1155 }
1156 Some(TypeExpr::Applied { name, args })
1157 if name == "Iter" && args.len() == 1 =>
1158 {
1159 if let TypeExpr::Applied { name: n2, args: a2 } = &args[0] {
1160 if n2 == "Pair" && a2.len() == 2 {
1161 (Some(a2[0].clone()), Some(a2[1].clone()))
1162 } else {
1163 (None, None)
1164 }
1165 } else {
1166 (None, None)
1167 }
1168 }
1169 _ => (None, None),
1170 };
1171 loop_scope.define_var(a, ka);
1172 loop_scope.define_var(b, vb);
1173 } else {
1174 self.check_pattern_defaults(pattern, &mut loop_scope);
1175 Self::define_pattern_vars(pattern, &mut loop_scope, false);
1176 }
1177 self.check_block(body, &mut loop_scope);
1178 }
1179
1180 Node::WhileLoop { condition, body } => {
1181 self.check_node(condition, scope);
1182 let refs = Self::extract_refinements(condition, scope);
1183 let mut loop_scope = scope.child();
1184 refs.apply_truthy(&mut loop_scope);
1185 self.check_block(body, &mut loop_scope);
1186 }
1187
1188 Node::RequireStmt { condition, message } => {
1189 self.check_node(condition, scope);
1190 if let Some(message) = message {
1191 self.check_node(message, scope);
1192 }
1193 }
1194
1195 Node::TryCatch {
1196 body,
1197 error_var,
1198 error_type,
1199 catch_body,
1200 finally_body,
1201 ..
1202 } => {
1203 let mut try_scope = scope.child();
1204 self.check_block(body, &mut try_scope);
1205 let mut catch_scope = scope.child();
1206 if let Some(var) = error_var {
1207 catch_scope.define_var(var, error_type.clone());
1208 }
1209 self.check_block(catch_body, &mut catch_scope);
1210 if let Some(fb) = finally_body {
1211 let mut finally_scope = scope.child();
1212 self.check_block(fb, &mut finally_scope);
1213 }
1214 }
1215
1216 Node::TryExpr { body } => {
1217 let mut try_scope = scope.child();
1218 self.check_block(body, &mut try_scope);
1219 }
1220
1221 Node::TryStar { operand } => {
1222 if self.fn_depth == 0 {
1223 self.error_at(
1224 "try* requires an enclosing function (fn, tool, or pipeline) so the rethrow has a target".to_string(),
1225 span,
1226 );
1227 }
1228 self.check_node(operand, scope);
1229 }
1230
1231 Node::ReturnStmt {
1232 value: Some(val), ..
1233 } => {
1234 self.check_node(val, scope);
1235 }
1236
1237 Node::Assignment {
1238 target, value, op, ..
1239 } => {
1240 self.check_node(value, scope);
1241 if let Node::Identifier(name) = &target.node {
1242 if scope.get_var(name).is_some() && !scope.is_mutable(name) {
1244 self.warning_at(
1245 format!(
1246 "Cannot assign to '{}': variable is immutable (declared with 'let')",
1247 name
1248 ),
1249 span,
1250 );
1251 }
1252
1253 if let Some(Some(var_type)) = scope.get_var(name) {
1254 let value_type = self.infer_type(value, scope);
1255 let assigned = if let Some(op) = op {
1256 let var_inferred = scope.get_var(name).cloned().flatten();
1257 infer_binary_op_type(op, &var_inferred, &value_type)
1258 } else {
1259 value_type
1260 };
1261 if let Some(actual) = &assigned {
1262 let check_type = scope
1264 .narrowed_vars
1265 .get(name)
1266 .and_then(|t| t.as_ref())
1267 .unwrap_or(var_type);
1268 if !self.types_compatible(check_type, actual, scope) {
1269 self.error_at(
1270 format!(
1271 "can't assign {} to '{}' (declared as {})",
1272 format_type(actual),
1273 name,
1274 format_type(check_type)
1275 ),
1276 span,
1277 );
1278 }
1279 }
1280 }
1281
1282 if let Some(original) = scope.narrowed_vars.remove(name) {
1284 scope.define_var(name, original);
1285 }
1286 scope.define_schema_binding(name, None);
1287 scope.clear_unknown_ruled_out(name);
1288 }
1289 }
1290
1291 Node::TypeDecl {
1292 name,
1293 type_params,
1294 type_expr,
1295 } => {
1296 scope.type_aliases.insert(name.clone(), type_expr.clone());
1297 self.check_type_alias_decl_variance(type_params, type_expr, name, span);
1298 }
1299
1300 Node::EnumDecl {
1301 name,
1302 type_params,
1303 variants,
1304 ..
1305 } => {
1306 scope.enums.insert(
1307 name.clone(),
1308 EnumDeclInfo {
1309 type_params: type_params.clone(),
1310 variants: variants.clone(),
1311 },
1312 );
1313 self.check_enum_decl_variance(type_params, variants, name, span);
1314 }
1315
1316 Node::StructDecl {
1317 name,
1318 type_params,
1319 fields,
1320 ..
1321 } => {
1322 scope.structs.insert(
1323 name.clone(),
1324 StructDeclInfo {
1325 type_params: type_params.clone(),
1326 fields: fields.clone(),
1327 },
1328 );
1329 self.check_struct_decl_variance(type_params, fields, name, span);
1330 }
1331
1332 Node::InterfaceDecl {
1333 name,
1334 type_params,
1335 associated_types,
1336 methods,
1337 } => {
1338 scope.interfaces.insert(
1339 name.clone(),
1340 InterfaceDeclInfo {
1341 type_params: type_params.clone(),
1342 associated_types: associated_types.clone(),
1343 methods: methods.clone(),
1344 },
1345 );
1346 self.check_interface_decl_variance(type_params, methods, name, span);
1347 }
1348
1349 Node::ImplBlock {
1350 type_name, methods, ..
1351 } => {
1352 let sigs: Vec<ImplMethodSig> = methods
1354 .iter()
1355 .filter_map(|m| {
1356 if let Node::FnDecl {
1357 name,
1358 params,
1359 return_type,
1360 ..
1361 } = &m.node
1362 {
1363 let non_self: Vec<_> =
1364 params.iter().filter(|p| p.name != "self").collect();
1365 let param_count = non_self.len();
1366 let param_types: Vec<Option<TypeExpr>> =
1367 non_self.iter().map(|p| p.type_expr.clone()).collect();
1368 Some(ImplMethodSig {
1369 name: name.clone(),
1370 param_count,
1371 param_types,
1372 return_type: return_type.clone(),
1373 })
1374 } else {
1375 None
1376 }
1377 })
1378 .collect();
1379 scope.impl_methods.insert(type_name.clone(), sigs);
1380 for method_sn in methods {
1381 self.check_node(method_sn, scope);
1382 }
1383 }
1384
1385 Node::TryOperator { operand } => {
1386 self.check_node(operand, scope);
1387 }
1388
1389 Node::MatchExpr { value, arms } => {
1390 self.check_node(value, scope);
1391 let value_type = self.infer_type(value, scope);
1392 for arm in arms {
1393 self.check_node(&arm.pattern, scope);
1394 if let Some(ref vt) = value_type {
1396 let value_type_name = format_type(vt);
1397 let mismatch = match &arm.pattern.node {
1398 Node::StringLiteral(_) => {
1399 !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
1400 }
1401 Node::IntLiteral(_) => {
1402 !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
1403 && !self.types_compatible(
1404 vt,
1405 &TypeExpr::Named("float".into()),
1406 scope,
1407 )
1408 }
1409 Node::FloatLiteral(_) => {
1410 !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
1411 && !self.types_compatible(
1412 vt,
1413 &TypeExpr::Named("int".into()),
1414 scope,
1415 )
1416 }
1417 Node::BoolLiteral(_) => {
1418 !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
1419 }
1420 _ => false,
1421 };
1422 if mismatch {
1423 let pattern_type = match &arm.pattern.node {
1424 Node::StringLiteral(_) => "string",
1425 Node::IntLiteral(_) => "int",
1426 Node::FloatLiteral(_) => "float",
1427 Node::BoolLiteral(_) => "bool",
1428 _ => unreachable!(),
1429 };
1430 self.warning_at(
1431 format!(
1432 "Match pattern type mismatch: matching {} against {} literal",
1433 value_type_name, pattern_type
1434 ),
1435 arm.pattern.span,
1436 );
1437 }
1438 }
1439 let mut arm_scope = scope.child();
1440 if let Node::Identifier(var_name) = &value.node {
1442 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(var_name) {
1443 let narrowed = match &arm.pattern.node {
1444 Node::NilLiteral => narrow_to_single(members, "nil"),
1445 Node::StringLiteral(_) => narrow_to_single(members, "string"),
1446 Node::IntLiteral(_) => narrow_to_single(members, "int"),
1447 Node::FloatLiteral(_) => narrow_to_single(members, "float"),
1448 Node::BoolLiteral(_) => narrow_to_single(members, "bool"),
1449 _ => None,
1450 };
1451 if let Some(narrowed_type) = narrowed {
1452 arm_scope.define_var(var_name, Some(narrowed_type));
1453 }
1454 }
1455 }
1456 if let Some(ref guard) = arm.guard {
1457 self.check_node(guard, &mut arm_scope);
1458 }
1459 self.check_block(&arm.body, &mut arm_scope);
1460 }
1461 self.check_match_exhaustiveness(value, arms, scope, span);
1462 }
1463
1464 Node::BinaryOp { op, left, right } => {
1466 self.check_node(left, scope);
1467 self.check_node(right, scope);
1468 let lt = self.infer_type(left, scope);
1470 let rt = self.infer_type(right, scope);
1471 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
1472 match op.as_str() {
1473 "-" | "/" | "%" | "**" => {
1474 let numeric = ["int", "float"];
1475 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
1476 self.error_at(
1477 format!(
1478 "can't use '{}' on {} and {} (needs numeric operands)",
1479 op, l, r
1480 ),
1481 span,
1482 );
1483 }
1484 }
1485 "*" => {
1486 let numeric = ["int", "float"];
1487 let is_numeric =
1488 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
1489 let is_string_repeat =
1490 (l == "string" && r == "int") || (l == "int" && r == "string");
1491 if !is_numeric && !is_string_repeat {
1492 self.error_at(
1493 format!("can't multiply {} and {} (try string * int)", l, r),
1494 span,
1495 );
1496 }
1497 }
1498 "+" => {
1499 let valid = matches!(
1500 (l.as_str(), r.as_str()),
1501 ("int" | "float", "int" | "float")
1502 | ("string", "string")
1503 | ("list", "list")
1504 | ("dict", "dict")
1505 );
1506 if !valid {
1507 let msg = format!("can't add {} and {}", l, r);
1508 let fix = if l == "string" || r == "string" {
1510 self.build_interpolation_fix(left, right, l == "string", span)
1511 } else {
1512 None
1513 };
1514 if let Some(fix) = fix {
1515 self.error_at_with_fix(msg, span, fix);
1516 } else {
1517 self.error_at(msg, span);
1518 }
1519 }
1520 }
1521 "<" | ">" | "<=" | ">=" => {
1522 let comparable = ["int", "float", "string"];
1523 if !comparable.contains(&l.as_str())
1524 || !comparable.contains(&r.as_str())
1525 {
1526 self.warning_at(
1527 format!(
1528 "Comparison '{}' may not be meaningful for types {} and {}",
1529 op, l, r
1530 ),
1531 span,
1532 );
1533 } else if (l == "string") != (r == "string") {
1534 self.warning_at(
1535 format!(
1536 "Comparing {} with {} using '{}' may give unexpected results",
1537 l, r, op
1538 ),
1539 span,
1540 );
1541 }
1542 }
1543 _ => {}
1544 }
1545 }
1546 }
1547 Node::UnaryOp { operand, .. } => {
1548 self.check_node(operand, scope);
1549 }
1550 Node::MethodCall {
1551 object,
1552 method,
1553 args,
1554 ..
1555 }
1556 | Node::OptionalMethodCall {
1557 object,
1558 method,
1559 args,
1560 ..
1561 } => {
1562 self.check_node(object, scope);
1563 for arg in args {
1564 self.check_node(arg, scope);
1565 }
1566 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
1570 if scope.is_generic_type_param(&type_name) {
1571 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
1572 if let Some(iface_methods) = scope.get_interface(iface_name) {
1573 let has_method =
1574 iface_methods.methods.iter().any(|m| m.name == *method);
1575 if !has_method {
1576 self.warning_at(
1577 format!(
1578 "Method '{}' not found in interface '{}' (constraint on '{}')",
1579 method, iface_name, type_name
1580 ),
1581 span,
1582 );
1583 }
1584 }
1585 }
1586 }
1587 }
1588 }
1589 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1590 if self.strict_types {
1591 if let Node::FunctionCall { name, args } = &object.node {
1593 if builtin_signatures::is_untyped_boundary_source(name) {
1594 let has_schema = (name == "llm_call" || name == "llm_completion")
1595 && Self::llm_call_has_typed_schema_option(args, scope);
1596 if !has_schema {
1597 self.warning_at_with_help(
1598 format!(
1599 "Direct property access on unvalidated `{}()` result",
1600 name
1601 ),
1602 span,
1603 "assign to a variable and validate with schema_expect() or a type annotation first".to_string(),
1604 );
1605 }
1606 }
1607 }
1608 if let Node::Identifier(name) = &object.node {
1610 if let Some(source) = scope.is_untyped_source(name) {
1611 self.warning_at_with_help(
1612 format!(
1613 "Accessing property on unvalidated value '{}' from `{}`",
1614 name, source
1615 ),
1616 span,
1617 "validate with schema_expect(), schema_is() in an if-condition, or add a shape type annotation".to_string(),
1618 );
1619 }
1620 }
1621 }
1622 self.check_node(object, scope);
1623 }
1624 Node::SubscriptAccess { object, index } => {
1625 if self.strict_types {
1626 if let Node::FunctionCall { name, args } = &object.node {
1627 if builtin_signatures::is_untyped_boundary_source(name) {
1628 let has_schema = (name == "llm_call" || name == "llm_completion")
1629 && Self::llm_call_has_typed_schema_option(args, scope);
1630 if !has_schema {
1631 self.warning_at_with_help(
1632 format!(
1633 "Direct subscript access on unvalidated `{}()` result",
1634 name
1635 ),
1636 span,
1637 "assign to a variable and validate with schema_expect() or a type annotation first".to_string(),
1638 );
1639 }
1640 }
1641 }
1642 if let Node::Identifier(name) = &object.node {
1643 if let Some(source) = scope.is_untyped_source(name) {
1644 self.warning_at_with_help(
1645 format!(
1646 "Subscript access on unvalidated value '{}' from `{}`",
1647 name, source
1648 ),
1649 span,
1650 "validate with schema_expect(), schema_is() in an if-condition, or add a shape type annotation".to_string(),
1651 );
1652 }
1653 }
1654 }
1655 self.check_node(object, scope);
1656 self.check_node(index, scope);
1657 }
1658 Node::SliceAccess { object, start, end } => {
1659 self.check_node(object, scope);
1660 if let Some(s) = start {
1661 self.check_node(s, scope);
1662 }
1663 if let Some(e) = end {
1664 self.check_node(e, scope);
1665 }
1666 }
1667
1668 Node::Ternary {
1669 condition,
1670 true_expr,
1671 false_expr,
1672 } => {
1673 self.check_node(condition, scope);
1674 let refs = Self::extract_refinements(condition, scope);
1675
1676 let mut true_scope = scope.child();
1677 refs.apply_truthy(&mut true_scope);
1678 self.check_node(true_expr, &mut true_scope);
1679
1680 let mut false_scope = scope.child();
1681 refs.apply_falsy(&mut false_scope);
1682 self.check_node(false_expr, &mut false_scope);
1683 }
1684
1685 Node::ThrowStmt { value } => {
1686 self.check_node(value, scope);
1687 self.check_unknown_exhaustiveness(scope, snode.span, "throw");
1691 }
1692
1693 Node::GuardStmt {
1694 condition,
1695 else_body,
1696 } => {
1697 self.check_node(condition, scope);
1698 let refs = Self::extract_refinements(condition, scope);
1699
1700 let mut else_scope = scope.child();
1701 refs.apply_falsy(&mut else_scope);
1702 self.check_block(else_body, &mut else_scope);
1703
1704 refs.apply_truthy(scope);
1707 }
1708
1709 Node::SpawnExpr { body } => {
1710 let mut spawn_scope = scope.child();
1711 self.check_block(body, &mut spawn_scope);
1712 }
1713
1714 Node::Parallel {
1715 mode,
1716 expr,
1717 variable,
1718 body,
1719 options,
1720 } => {
1721 self.check_node(expr, scope);
1722 for (key, value) in options {
1723 self.check_node(value, scope);
1728 if key == "max_concurrent" {
1729 if let Some(ty) = self.infer_type(value, scope) {
1730 if !matches!(ty, TypeExpr::Named(ref n) if n == "int") {
1731 self.error_at(
1732 format!(
1733 "`max_concurrent` on `parallel` must be int, got {ty:?}"
1734 ),
1735 value.span,
1736 );
1737 }
1738 }
1739 }
1740 }
1741 let mut par_scope = scope.child();
1742 if let Some(var) = variable {
1743 let var_type = match mode {
1744 ParallelMode::Count => Some(TypeExpr::Named("int".into())),
1745 ParallelMode::Each | ParallelMode::Settle => {
1746 match self.infer_type(expr, scope) {
1747 Some(TypeExpr::List(inner)) => Some(*inner),
1748 _ => None,
1749 }
1750 }
1751 };
1752 par_scope.define_var(var, var_type);
1753 }
1754 self.check_block(body, &mut par_scope);
1755 }
1756
1757 Node::SelectExpr {
1758 cases,
1759 timeout,
1760 default_body,
1761 } => {
1762 for case in cases {
1763 self.check_node(&case.channel, scope);
1764 let mut case_scope = scope.child();
1765 case_scope.define_var(&case.variable, None);
1766 self.check_block(&case.body, &mut case_scope);
1767 }
1768 if let Some((dur, body)) = timeout {
1769 self.check_node(dur, scope);
1770 let mut timeout_scope = scope.child();
1771 self.check_block(body, &mut timeout_scope);
1772 }
1773 if let Some(body) = default_body {
1774 let mut default_scope = scope.child();
1775 self.check_block(body, &mut default_scope);
1776 }
1777 }
1778
1779 Node::DeadlineBlock { duration, body } => {
1780 self.check_node(duration, scope);
1781 let mut block_scope = scope.child();
1782 self.check_block(body, &mut block_scope);
1783 }
1784
1785 Node::MutexBlock { body } | Node::DeferStmt { body } => {
1786 let mut block_scope = scope.child();
1787 self.check_block(body, &mut block_scope);
1788 }
1789
1790 Node::Retry { count, body } => {
1791 self.check_node(count, scope);
1792 let mut retry_scope = scope.child();
1793 self.check_block(body, &mut retry_scope);
1794 }
1795
1796 Node::Closure { params, body, .. } => {
1797 let mut closure_scope = scope.child();
1798 for p in params {
1799 closure_scope.define_var(&p.name, p.type_expr.clone());
1800 }
1801 self.fn_depth += 1;
1802 self.check_block(body, &mut closure_scope);
1803 self.fn_depth -= 1;
1804 }
1805
1806 Node::ListLiteral(elements) => {
1807 for elem in elements {
1808 self.check_node(elem, scope);
1809 }
1810 }
1811
1812 Node::DictLiteral(entries) => {
1813 for entry in entries {
1814 self.check_node(&entry.key, scope);
1815 self.check_node(&entry.value, scope);
1816 }
1817 }
1818
1819 Node::RangeExpr { start, end, .. } => {
1820 self.check_node(start, scope);
1821 self.check_node(end, scope);
1822 }
1823
1824 Node::Spread(inner) => {
1825 self.check_node(inner, scope);
1826 }
1827
1828 Node::Block(stmts) => {
1829 let mut block_scope = scope.child();
1830 self.check_block(stmts, &mut block_scope);
1831 }
1832
1833 Node::YieldExpr { value } => {
1834 if let Some(v) = value {
1835 self.check_node(v, scope);
1836 }
1837 }
1838
1839 Node::StructConstruct {
1840 struct_name,
1841 fields,
1842 } => {
1843 for entry in fields {
1844 self.check_node(&entry.key, scope);
1845 self.check_node(&entry.value, scope);
1846 }
1847 if let Some(struct_info) = scope.get_struct(struct_name).cloned() {
1848 let type_bindings = self.infer_struct_bindings(&struct_info, fields, scope);
1849 for entry in fields {
1851 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1852 if !struct_info.fields.iter().any(|field| field.name == *key) {
1853 self.warning_at(
1854 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1855 entry.key.span,
1856 );
1857 }
1858 }
1859 }
1860 let provided: Vec<String> = fields
1862 .iter()
1863 .filter_map(|e| match &e.key.node {
1864 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1865 _ => None,
1866 })
1867 .collect();
1868 for field in &struct_info.fields {
1869 if !field.optional && !provided.contains(&field.name) {
1870 self.warning_at(
1871 format!(
1872 "Missing field '{}' in struct '{}' construction",
1873 field.name, struct_name
1874 ),
1875 span,
1876 );
1877 }
1878 }
1879 for field in &struct_info.fields {
1880 let Some(expected_type) = &field.type_expr else {
1881 continue;
1882 };
1883 let Some(entry) = fields.iter().find(|entry| {
1884 matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
1885 }) else {
1886 continue;
1887 };
1888 let Some(actual_type) = self.infer_type(&entry.value, scope) else {
1889 continue;
1890 };
1891 let expected = Self::apply_type_bindings(expected_type, &type_bindings);
1892 if !self.types_compatible(&expected, &actual_type, scope) {
1893 self.error_at(
1894 format!(
1895 "Field '{}' in struct '{}' expects {}, got {}",
1896 field.name,
1897 struct_name,
1898 format_type(&expected),
1899 format_type(&actual_type)
1900 ),
1901 entry.value.span,
1902 );
1903 }
1904 }
1905 }
1906 }
1907
1908 Node::EnumConstruct {
1909 enum_name,
1910 variant,
1911 args,
1912 } => {
1913 for arg in args {
1914 self.check_node(arg, scope);
1915 }
1916 if let Some(enum_info) = scope.get_enum(enum_name).cloned() {
1917 let Some(enum_variant) = enum_info
1918 .variants
1919 .iter()
1920 .find(|enum_variant| enum_variant.name == *variant)
1921 else {
1922 self.warning_at(
1923 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1924 span,
1925 );
1926 return;
1927 };
1928 if args.len() != enum_variant.fields.len() {
1929 self.warning_at(
1930 format!(
1931 "{}.{} expects {} argument(s), got {}",
1932 enum_name,
1933 variant,
1934 enum_variant.fields.len(),
1935 args.len()
1936 ),
1937 span,
1938 );
1939 }
1940 let type_param_set: std::collections::BTreeSet<String> = enum_info
1941 .type_params
1942 .iter()
1943 .map(|tp| tp.name.clone())
1944 .collect();
1945 let mut type_bindings = BTreeMap::new();
1946 for (field, arg) in enum_variant.fields.iter().zip(args.iter()) {
1947 let Some(expected_type) = &field.type_expr else {
1948 continue;
1949 };
1950 let Some(actual_type) = self.infer_type(arg, scope) else {
1951 continue;
1952 };
1953 if let Err(message) = Self::extract_type_bindings(
1954 expected_type,
1955 &actual_type,
1956 &type_param_set,
1957 &mut type_bindings,
1958 ) {
1959 self.error_at(message, arg.span);
1960 }
1961 }
1962 for (field, arg) in enum_variant.fields.iter().zip(args.iter()) {
1963 let Some(expected_type) = &field.type_expr else {
1964 continue;
1965 };
1966 let Some(actual_type) = self.infer_type(arg, scope) else {
1967 continue;
1968 };
1969 let expected = Self::apply_type_bindings(expected_type, &type_bindings);
1970 if !self.types_compatible(&expected, &actual_type, scope) {
1971 self.error_at(
1972 format!(
1973 "{}.{} expects {}: {}, got {}",
1974 enum_name,
1975 variant,
1976 field.name,
1977 format_type(&expected),
1978 format_type(&actual_type)
1979 ),
1980 arg.span,
1981 );
1982 }
1983 }
1984 }
1985 }
1986
1987 Node::InterpolatedString(_) => {}
1988
1989 Node::StringLiteral(_)
1990 | Node::RawStringLiteral(_)
1991 | Node::IntLiteral(_)
1992 | Node::FloatLiteral(_)
1993 | Node::BoolLiteral(_)
1994 | Node::NilLiteral
1995 | Node::Identifier(_)
1996 | Node::DurationLiteral(_)
1997 | Node::BreakStmt
1998 | Node::ContinueStmt
1999 | Node::ReturnStmt { value: None }
2000 | Node::ImportDecl { .. }
2001 | Node::SelectiveImport { .. } => {}
2002
2003 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
2006 let mut decl_scope = scope.child();
2007 self.fn_depth += 1;
2008 self.check_block(body, &mut decl_scope);
2009 self.fn_depth -= 1;
2010 }
2011 Node::AttributedDecl { attributes, inner } => {
2012 self.check_attributes(attributes, inner);
2013 self.check_node(inner, scope);
2014 }
2015 }
2016 }
2017
2018 fn check_attributes(&mut self, attributes: &[Attribute], inner: &SNode) {
2023 for attr in attributes {
2024 match attr.name.as_str() {
2025 "deprecated" | "test" | "complexity" | "acp_tool" => {}
2026 other => {
2027 self.warning_at(format!("unknown attribute `@{}`", other), attr.span);
2028 }
2029 }
2030 if attr.name == "test" && !matches!(inner.node, Node::FnDecl { .. }) {
2032 self.warning_at(
2033 "`@test` only applies to function declarations".to_string(),
2034 attr.span,
2035 );
2036 }
2037 if attr.name == "acp_tool" && !matches!(inner.node, Node::FnDecl { .. }) {
2038 self.warning_at(
2039 "`@acp_tool` only applies to function declarations".to_string(),
2040 attr.span,
2041 );
2042 }
2043 }
2044 }
2045
2046 fn check_fn_body(
2047 &mut self,
2048 type_params: &[TypeParam],
2049 params: &[TypedParam],
2050 return_type: &Option<TypeExpr>,
2051 body: &[SNode],
2052 where_clauses: &[WhereClause],
2053 ) {
2054 self.fn_depth += 1;
2055 self.check_fn_body_inner(type_params, params, return_type, body, where_clauses);
2056 self.fn_depth -= 1;
2057 }
2058
2059 fn check_fn_body_inner(
2060 &mut self,
2061 type_params: &[TypeParam],
2062 params: &[TypedParam],
2063 return_type: &Option<TypeExpr>,
2064 body: &[SNode],
2065 where_clauses: &[WhereClause],
2066 ) {
2067 let mut fn_scope = self.scope.child();
2068 for tp in type_params {
2071 fn_scope.generic_type_params.insert(tp.name.clone());
2072 }
2073 for wc in where_clauses {
2075 fn_scope
2076 .where_constraints
2077 .insert(wc.type_name.clone(), wc.bound.clone());
2078 }
2079 for param in params {
2080 fn_scope.define_var(¶m.name, param.type_expr.clone());
2081 if let Some(default) = ¶m.default_value {
2082 self.check_node(default, &mut fn_scope);
2083 }
2084 }
2085 let ret_scope_base = if return_type.is_some() {
2088 Some(fn_scope.child())
2089 } else {
2090 None
2091 };
2092
2093 self.check_block(body, &mut fn_scope);
2094
2095 if let Some(ret_type) = return_type {
2097 let mut ret_scope = ret_scope_base.unwrap();
2098 for stmt in body {
2099 self.check_return_type(stmt, ret_type, &mut ret_scope);
2100 }
2101 }
2102 }
2103
2104 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
2105 let span = snode.span;
2106 match &snode.node {
2107 Node::ReturnStmt { value: Some(val) } => {
2108 let inferred = self.infer_type(val, scope);
2109 if let Some(actual) = &inferred {
2110 if !self.types_compatible(expected, actual, scope) {
2111 self.error_at(
2112 format!(
2113 "return type doesn't match: expected {}, got {}",
2114 format_type(expected),
2115 format_type(actual)
2116 ),
2117 span,
2118 );
2119 }
2120 }
2121 }
2122 Node::IfElse {
2123 condition,
2124 then_body,
2125 else_body,
2126 } => {
2127 let refs = Self::extract_refinements(condition, scope);
2128 let mut then_scope = scope.child();
2129 refs.apply_truthy(&mut then_scope);
2130 for stmt in then_body {
2131 self.check_return_type(stmt, expected, &mut then_scope);
2132 }
2133 if let Some(else_body) = else_body {
2134 let mut else_scope = scope.child();
2135 refs.apply_falsy(&mut else_scope);
2136 for stmt in else_body {
2137 self.check_return_type(stmt, expected, &mut else_scope);
2138 }
2139 if Self::block_definitely_exits(then_body)
2141 && !Self::block_definitely_exits(else_body)
2142 {
2143 refs.apply_falsy(scope);
2144 } else if Self::block_definitely_exits(else_body)
2145 && !Self::block_definitely_exits(then_body)
2146 {
2147 refs.apply_truthy(scope);
2148 }
2149 } else {
2150 if Self::block_definitely_exits(then_body) {
2152 refs.apply_falsy(scope);
2153 }
2154 }
2155 }
2156 _ => {}
2157 }
2158 }
2159
2160 fn satisfies_interface(
2166 &self,
2167 type_name: &str,
2168 interface_name: &str,
2169 interface_bindings: &BTreeMap<String, TypeExpr>,
2170 scope: &TypeScope,
2171 ) -> bool {
2172 self.interface_mismatch_reason(type_name, interface_name, interface_bindings, scope)
2173 .is_none()
2174 }
2175
2176 fn interface_mismatch_reason(
2179 &self,
2180 type_name: &str,
2181 interface_name: &str,
2182 interface_bindings: &BTreeMap<String, TypeExpr>,
2183 scope: &TypeScope,
2184 ) -> Option<String> {
2185 let interface_info = match scope.get_interface(interface_name) {
2186 Some(info) => info,
2187 None => return Some(format!("interface '{}' not found", interface_name)),
2188 };
2189 let impl_methods = match scope.get_impl_methods(type_name) {
2190 Some(methods) => methods,
2191 None => {
2192 if interface_info.methods.is_empty() {
2193 return None;
2194 }
2195 let names: Vec<_> = interface_info
2196 .methods
2197 .iter()
2198 .map(|m| m.name.as_str())
2199 .collect();
2200 return Some(format!("missing method(s): {}", names.join(", ")));
2201 }
2202 };
2203 let mut bindings = interface_bindings.clone();
2204 let associated_type_names: std::collections::BTreeSet<String> = interface_info
2205 .associated_types
2206 .iter()
2207 .map(|(name, _)| name.clone())
2208 .collect();
2209 for iface_method in &interface_info.methods {
2210 let iface_params: Vec<_> = iface_method
2211 .params
2212 .iter()
2213 .filter(|p| p.name != "self")
2214 .collect();
2215 let iface_param_count = iface_params.len();
2216 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
2217 let impl_method = match matching_impl {
2218 Some(m) => m,
2219 None => {
2220 return Some(format!("missing method '{}'", iface_method.name));
2221 }
2222 };
2223 if impl_method.param_count != iface_param_count {
2224 return Some(format!(
2225 "method '{}' has {} parameter(s), expected {}",
2226 iface_method.name, impl_method.param_count, iface_param_count
2227 ));
2228 }
2229 for (i, iface_param) in iface_params.iter().enumerate() {
2231 if let (Some(expected), Some(actual)) = (
2232 &iface_param.type_expr,
2233 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
2234 ) {
2235 if let Err(message) = Self::extract_type_bindings(
2236 expected,
2237 actual,
2238 &associated_type_names,
2239 &mut bindings,
2240 ) {
2241 return Some(message);
2242 }
2243 let expected = Self::apply_type_bindings(expected, &bindings);
2244 if !self.types_compatible(&expected, actual, scope) {
2245 return Some(format!(
2246 "method '{}' parameter {} has type '{}', expected '{}'",
2247 iface_method.name,
2248 i + 1,
2249 format_type(actual),
2250 format_type(&expected),
2251 ));
2252 }
2253 }
2254 }
2255 if let (Some(expected_ret), Some(actual_ret)) =
2257 (&iface_method.return_type, &impl_method.return_type)
2258 {
2259 if let Err(message) = Self::extract_type_bindings(
2260 expected_ret,
2261 actual_ret,
2262 &associated_type_names,
2263 &mut bindings,
2264 ) {
2265 return Some(message);
2266 }
2267 let expected_ret = Self::apply_type_bindings(expected_ret, &bindings);
2268 if !self.types_compatible(&expected_ret, actual_ret, scope) {
2269 return Some(format!(
2270 "method '{}' returns '{}', expected '{}'",
2271 iface_method.name,
2272 format_type(actual_ret),
2273 format_type(&expected_ret),
2274 ));
2275 }
2276 }
2277 }
2278 for (assoc_name, default_type) in &interface_info.associated_types {
2279 if let (Some(default_type), Some(actual)) = (default_type, bindings.get(assoc_name)) {
2280 let expected = Self::apply_type_bindings(default_type, &bindings);
2281 if !self.types_compatible(&expected, actual, scope) {
2282 return Some(format!(
2283 "associated type '{}' resolves to '{}', expected '{}'",
2284 assoc_name,
2285 format_type(actual),
2286 format_type(&expected),
2287 ));
2288 }
2289 }
2290 }
2291 None
2292 }
2293
2294 fn bind_type_param(
2295 param_name: &str,
2296 concrete: &TypeExpr,
2297 bindings: &mut BTreeMap<String, TypeExpr>,
2298 ) -> Result<(), String> {
2299 if let Some(existing) = bindings.get(param_name) {
2300 if existing != concrete {
2301 return Err(format!(
2302 "type parameter '{}' was inferred as both {} and {}",
2303 param_name,
2304 format_type(existing),
2305 format_type(concrete)
2306 ));
2307 }
2308 return Ok(());
2309 }
2310 bindings.insert(param_name.to_string(), concrete.clone());
2311 Ok(())
2312 }
2313
2314 fn extract_type_bindings(
2317 param_type: &TypeExpr,
2318 arg_type: &TypeExpr,
2319 type_params: &std::collections::BTreeSet<String>,
2320 bindings: &mut BTreeMap<String, TypeExpr>,
2321 ) -> Result<(), String> {
2322 match (param_type, arg_type) {
2323 (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
2324 Self::bind_type_param(param_name, concrete, bindings)
2325 }
2326 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
2327 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
2328 }
2329 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
2330 Self::extract_type_bindings(pk, ak, type_params, bindings)?;
2331 Self::extract_type_bindings(pv, av, type_params, bindings)
2332 }
2333 (
2334 TypeExpr::Applied {
2335 name: p_name,
2336 args: p_args,
2337 },
2338 TypeExpr::Applied {
2339 name: a_name,
2340 args: a_args,
2341 },
2342 ) if p_name == a_name && p_args.len() == a_args.len() => {
2343 for (param, arg) in p_args.iter().zip(a_args.iter()) {
2344 Self::extract_type_bindings(param, arg, type_params, bindings)?;
2345 }
2346 Ok(())
2347 }
2348 (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
2349 for param_field in param_fields {
2350 if let Some(arg_field) = arg_fields
2351 .iter()
2352 .find(|field| field.name == param_field.name)
2353 {
2354 Self::extract_type_bindings(
2355 ¶m_field.type_expr,
2356 &arg_field.type_expr,
2357 type_params,
2358 bindings,
2359 )?;
2360 }
2361 }
2362 Ok(())
2363 }
2364 (
2365 TypeExpr::FnType {
2366 params: p_params,
2367 return_type: p_ret,
2368 },
2369 TypeExpr::FnType {
2370 params: a_params,
2371 return_type: a_ret,
2372 },
2373 ) => {
2374 for (param, arg) in p_params.iter().zip(a_params.iter()) {
2375 Self::extract_type_bindings(param, arg, type_params, bindings)?;
2376 }
2377 Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
2378 }
2379 _ => Ok(()),
2380 }
2381 }
2382
2383 fn bind_from_arg_node(
2395 &self,
2396 param: &TypeExpr,
2397 arg: &SNode,
2398 type_params: &std::collections::BTreeSet<String>,
2399 bindings: &mut BTreeMap<String, TypeExpr>,
2400 scope: &TypeScope,
2401 ) -> Result<(), String> {
2402 match param {
2403 TypeExpr::Applied { name, args } if name == "Schema" && args.len() == 1 => {
2404 if let TypeExpr::Named(tp) = &args[0] {
2405 if type_params.contains(tp) {
2406 if let Some(resolved) = schema_type_expr_from_node(arg, scope) {
2407 Self::bind_type_param(tp, &resolved, bindings)?;
2408 }
2409 }
2410 }
2411 Ok(())
2412 }
2413 TypeExpr::Shape(fields) => {
2414 if let Node::DictLiteral(entries) = &arg.node {
2415 for field in fields {
2416 let matching = entries.iter().find(|entry| match &entry.key.node {
2417 Node::StringLiteral(key) | Node::Identifier(key) => key == &field.name,
2418 _ => false,
2419 });
2420 if let Some(entry) = matching {
2421 self.bind_from_arg_node(
2422 &field.type_expr,
2423 &entry.value,
2424 type_params,
2425 bindings,
2426 scope,
2427 )?;
2428 }
2429 }
2430 return Ok(());
2431 }
2432 if let Some(arg_ty) = self.infer_type(arg, scope) {
2433 Self::extract_type_bindings(param, &arg_ty, type_params, bindings)?;
2434 }
2435 Ok(())
2436 }
2437 _ => {
2438 if let Some(arg_ty) = self.infer_type(arg, scope) {
2439 Self::extract_type_bindings(param, &arg_ty, type_params, bindings)?;
2440 }
2441 Ok(())
2442 }
2443 }
2444 }
2445
2446 fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
2447 match ty {
2448 TypeExpr::Named(name) => bindings
2449 .get(name)
2450 .cloned()
2451 .unwrap_or_else(|| TypeExpr::Named(name.clone())),
2452 TypeExpr::Union(items) => TypeExpr::Union(
2453 items
2454 .iter()
2455 .map(|item| Self::apply_type_bindings(item, bindings))
2456 .collect(),
2457 ),
2458 TypeExpr::Shape(fields) => TypeExpr::Shape(
2459 fields
2460 .iter()
2461 .map(|field| ShapeField {
2462 name: field.name.clone(),
2463 type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
2464 optional: field.optional,
2465 })
2466 .collect(),
2467 ),
2468 TypeExpr::List(inner) => {
2469 TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
2470 }
2471 TypeExpr::Iter(inner) => {
2472 TypeExpr::Iter(Box::new(Self::apply_type_bindings(inner, bindings)))
2473 }
2474 TypeExpr::DictType(key, value) => TypeExpr::DictType(
2475 Box::new(Self::apply_type_bindings(key, bindings)),
2476 Box::new(Self::apply_type_bindings(value, bindings)),
2477 ),
2478 TypeExpr::Applied { name, args } => TypeExpr::Applied {
2479 name: name.clone(),
2480 args: args
2481 .iter()
2482 .map(|arg| Self::apply_type_bindings(arg, bindings))
2483 .collect(),
2484 },
2485 TypeExpr::FnType {
2486 params,
2487 return_type,
2488 } => TypeExpr::FnType {
2489 params: params
2490 .iter()
2491 .map(|param| Self::apply_type_bindings(param, bindings))
2492 .collect(),
2493 return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
2494 },
2495 TypeExpr::Never => TypeExpr::Never,
2496 TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
2497 TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
2498 }
2499 }
2500
2501 fn applied_type_or_name(name: &str, args: Vec<TypeExpr>) -> TypeExpr {
2502 if args.is_empty() {
2503 TypeExpr::Named(name.to_string())
2504 } else {
2505 TypeExpr::Applied {
2506 name: name.to_string(),
2507 args,
2508 }
2509 }
2510 }
2511
2512 fn infer_struct_bindings(
2513 &self,
2514 struct_info: &StructDeclInfo,
2515 fields: &[DictEntry],
2516 scope: &TypeScope,
2517 ) -> BTreeMap<String, TypeExpr> {
2518 let type_param_set: std::collections::BTreeSet<String> = struct_info
2519 .type_params
2520 .iter()
2521 .map(|tp| tp.name.clone())
2522 .collect();
2523 let mut bindings = BTreeMap::new();
2524 for field in &struct_info.fields {
2525 let Some(expected_type) = &field.type_expr else {
2526 continue;
2527 };
2528 let Some(entry) = fields.iter().find(|entry| {
2529 matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
2530 }) else {
2531 continue;
2532 };
2533 let Some(actual_type) = self.infer_type(&entry.value, scope) else {
2534 continue;
2535 };
2536 let _ = Self::extract_type_bindings(
2537 expected_type,
2538 &actual_type,
2539 &type_param_set,
2540 &mut bindings,
2541 );
2542 }
2543 bindings
2544 }
2545
2546 fn infer_struct_type(
2547 &self,
2548 struct_name: &str,
2549 struct_info: &StructDeclInfo,
2550 fields: &[DictEntry],
2551 scope: &TypeScope,
2552 ) -> TypeExpr {
2553 let bindings = self.infer_struct_bindings(struct_info, fields, scope);
2554 let args = struct_info
2555 .type_params
2556 .iter()
2557 .map(|tp| {
2558 bindings
2559 .get(&tp.name)
2560 .cloned()
2561 .unwrap_or_else(Self::wildcard_type)
2562 })
2563 .collect();
2564 Self::applied_type_or_name(struct_name, args)
2565 }
2566
2567 fn infer_enum_type(
2568 &self,
2569 enum_name: &str,
2570 enum_info: &EnumDeclInfo,
2571 variant_name: &str,
2572 args: &[SNode],
2573 scope: &TypeScope,
2574 ) -> TypeExpr {
2575 let type_param_set: std::collections::BTreeSet<String> = enum_info
2576 .type_params
2577 .iter()
2578 .map(|tp| tp.name.clone())
2579 .collect();
2580 let mut bindings = BTreeMap::new();
2581 if let Some(variant) = enum_info
2582 .variants
2583 .iter()
2584 .find(|variant| variant.name == variant_name)
2585 {
2586 for (field, arg) in variant.fields.iter().zip(args.iter()) {
2587 let Some(expected_type) = &field.type_expr else {
2588 continue;
2589 };
2590 let Some(actual_type) = self.infer_type(arg, scope) else {
2591 continue;
2592 };
2593 let _ = Self::extract_type_bindings(
2594 expected_type,
2595 &actual_type,
2596 &type_param_set,
2597 &mut bindings,
2598 );
2599 }
2600 }
2601 let args = enum_info
2602 .type_params
2603 .iter()
2604 .map(|tp| {
2605 bindings
2606 .get(&tp.name)
2607 .cloned()
2608 .unwrap_or_else(Self::wildcard_type)
2609 })
2610 .collect();
2611 Self::applied_type_or_name(enum_name, args)
2612 }
2613
2614 fn infer_try_error_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
2615 let mut inferred: Vec<TypeExpr> = Vec::new();
2616 for stmt in stmts {
2617 match &stmt.node {
2618 Node::ThrowStmt { value } => {
2619 if let Some(ty) = self.infer_type(value, scope) {
2620 inferred.push(ty);
2621 }
2622 }
2623 Node::TryOperator { operand } => {
2624 if let Some(TypeExpr::Applied { name, args }) = self.infer_type(operand, scope)
2625 {
2626 if name == "Result" && args.len() == 2 {
2627 inferred.push(args[1].clone());
2628 }
2629 }
2630 }
2631 Node::IfElse {
2632 then_body,
2633 else_body,
2634 ..
2635 } => {
2636 if let Some(ty) = self.infer_try_error_type(then_body, scope) {
2637 inferred.push(ty);
2638 }
2639 if let Some(else_body) = else_body {
2640 if let Some(ty) = self.infer_try_error_type(else_body, scope) {
2641 inferred.push(ty);
2642 }
2643 }
2644 }
2645 Node::Block(body)
2646 | Node::TryExpr { body }
2647 | Node::SpawnExpr { body }
2648 | Node::Retry { body, .. }
2649 | Node::WhileLoop { body, .. }
2650 | Node::DeferStmt { body }
2651 | Node::MutexBlock { body }
2652 | Node::DeadlineBlock { body, .. }
2653 | Node::Pipeline { body, .. }
2654 | Node::OverrideDecl { body, .. } => {
2655 if let Some(ty) = self.infer_try_error_type(body, scope) {
2656 inferred.push(ty);
2657 }
2658 }
2659 _ => {}
2660 }
2661 }
2662 if inferred.is_empty() {
2663 None
2664 } else {
2665 Some(simplify_union(inferred))
2666 }
2667 }
2668
2669 fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
2670 let mut inferred: Option<TypeExpr> = None;
2671 for item in items {
2672 let Some(item_type) = self.infer_type(item, scope) else {
2673 return TypeExpr::Named("list".into());
2674 };
2675 inferred = Some(match inferred {
2676 None => item_type,
2677 Some(current) if current == item_type => current,
2678 Some(TypeExpr::Union(mut members)) => {
2679 if !members.contains(&item_type) {
2680 members.push(item_type);
2681 }
2682 TypeExpr::Union(members)
2683 }
2684 Some(current) => TypeExpr::Union(vec![current, item_type]),
2685 });
2686 }
2687 inferred
2688 .map(|item_type| TypeExpr::List(Box::new(item_type)))
2689 .unwrap_or_else(|| TypeExpr::Named("list".into()))
2690 }
2691
2692 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
2694 match &condition.node {
2695 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
2696 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
2697 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
2698 return nil_ref;
2699 }
2700 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
2701 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
2702 return typeof_ref;
2703 }
2704 Refinements::empty()
2705 }
2706
2707 Node::BinaryOp { op, left, right } if op == "&&" => {
2709 let left_ref = Self::extract_refinements(left, scope);
2710 let right_ref = Self::extract_refinements(right, scope);
2711 let mut truthy = left_ref.truthy;
2712 truthy.extend(right_ref.truthy);
2713 let mut truthy_ruled_out = left_ref.truthy_ruled_out;
2714 truthy_ruled_out.extend(right_ref.truthy_ruled_out);
2715 Refinements {
2716 truthy,
2717 falsy: vec![],
2718 truthy_ruled_out,
2719 falsy_ruled_out: vec![],
2720 }
2721 }
2722
2723 Node::BinaryOp { op, left, right } if op == "||" => {
2725 let left_ref = Self::extract_refinements(left, scope);
2726 let right_ref = Self::extract_refinements(right, scope);
2727 let mut falsy = left_ref.falsy;
2728 falsy.extend(right_ref.falsy);
2729 let mut falsy_ruled_out = left_ref.falsy_ruled_out;
2730 falsy_ruled_out.extend(right_ref.falsy_ruled_out);
2731 Refinements {
2732 truthy: vec![],
2733 falsy,
2734 truthy_ruled_out: vec![],
2735 falsy_ruled_out,
2736 }
2737 }
2738
2739 Node::UnaryOp { op, operand } if op == "!" => {
2740 Self::extract_refinements(operand, scope).inverted()
2741 }
2742
2743 Node::Identifier(name) => {
2745 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
2746 if members
2747 .iter()
2748 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
2749 {
2750 if let Some(narrowed) = remove_from_union(members, "nil") {
2751 return Refinements {
2752 truthy: vec![(name.clone(), Some(narrowed))],
2753 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2754 truthy_ruled_out: vec![],
2755 falsy_ruled_out: vec![],
2756 };
2757 }
2758 }
2759 }
2760 Refinements::empty()
2761 }
2762
2763 Node::MethodCall {
2764 object,
2765 method,
2766 args,
2767 } if method == "has" && args.len() == 1 => {
2768 Self::extract_has_refinements(object, args, scope)
2769 }
2770
2771 Node::FunctionCall { name, args }
2772 if (name == "schema_is" || name == "is_type") && args.len() == 2 =>
2773 {
2774 Self::extract_schema_refinements(args, scope)
2775 }
2776
2777 _ => Refinements::empty(),
2778 }
2779 }
2780
2781 fn extract_nil_refinements(
2783 op: &str,
2784 left: &SNode,
2785 right: &SNode,
2786 scope: &TypeScope,
2787 ) -> Refinements {
2788 let var_node = if matches!(right.node, Node::NilLiteral) {
2789 left
2790 } else if matches!(left.node, Node::NilLiteral) {
2791 right
2792 } else {
2793 return Refinements::empty();
2794 };
2795
2796 if let Node::Identifier(name) = &var_node.node {
2797 let var_type = scope.get_var(name).cloned().flatten();
2798 match var_type {
2799 Some(TypeExpr::Union(ref members)) => {
2800 if let Some(narrowed) = remove_from_union(members, "nil") {
2801 let neq_refs = Refinements {
2802 truthy: vec![(name.clone(), Some(narrowed))],
2803 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2804 ..Refinements::default()
2805 };
2806 return if op == "!=" {
2807 neq_refs
2808 } else {
2809 neq_refs.inverted()
2810 };
2811 }
2812 }
2813 Some(TypeExpr::Named(ref n)) if n == "nil" => {
2814 let eq_refs = Refinements {
2816 truthy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2817 falsy: vec![(name.clone(), Some(TypeExpr::Never))],
2818 ..Refinements::default()
2819 };
2820 return if op == "==" {
2821 eq_refs
2822 } else {
2823 eq_refs.inverted()
2824 };
2825 }
2826 _ => {}
2827 }
2828 }
2829 Refinements::empty()
2830 }
2831
2832 fn extract_typeof_refinements(
2834 op: &str,
2835 left: &SNode,
2836 right: &SNode,
2837 scope: &TypeScope,
2838 ) -> Refinements {
2839 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
2840 (extract_type_of_var(left), &right.node)
2841 {
2842 (var, tn.clone())
2843 } else if let (Node::StringLiteral(tn), Some(var)) =
2844 (&left.node, extract_type_of_var(right))
2845 {
2846 (var, tn.clone())
2847 } else {
2848 return Refinements::empty();
2849 };
2850
2851 const KNOWN_TYPES: &[&str] = &[
2852 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
2853 ];
2854 if !KNOWN_TYPES.contains(&type_name.as_str()) {
2855 return Refinements::empty();
2856 }
2857
2858 let var_type = scope.get_var(&var_name).cloned().flatten();
2859 match var_type {
2860 Some(TypeExpr::Union(ref members)) => {
2861 let narrowed = narrow_to_single(members, &type_name);
2862 let remaining = remove_from_union(members, &type_name);
2863 if narrowed.is_some() || remaining.is_some() {
2864 let eq_refs = Refinements {
2865 truthy: narrowed
2866 .map(|n| vec![(var_name.clone(), Some(n))])
2867 .unwrap_or_default(),
2868 falsy: remaining
2869 .map(|r| vec![(var_name.clone(), Some(r))])
2870 .unwrap_or_default(),
2871 ..Refinements::default()
2872 };
2873 return if op == "==" {
2874 eq_refs
2875 } else {
2876 eq_refs.inverted()
2877 };
2878 }
2879 }
2880 Some(TypeExpr::Named(ref n)) if n == &type_name => {
2881 let eq_refs = Refinements {
2884 truthy: vec![(var_name.clone(), Some(TypeExpr::Named(type_name)))],
2885 falsy: vec![(var_name.clone(), Some(TypeExpr::Never))],
2886 ..Refinements::default()
2887 };
2888 return if op == "==" {
2889 eq_refs
2890 } else {
2891 eq_refs.inverted()
2892 };
2893 }
2894 Some(TypeExpr::Named(ref n)) if n == "unknown" => {
2895 let eq_refs = Refinements {
2902 truthy: vec![(var_name.clone(), Some(TypeExpr::Named(type_name.clone())))],
2903 falsy: vec![],
2904 truthy_ruled_out: vec![],
2905 falsy_ruled_out: vec![(var_name.clone(), type_name)],
2906 };
2907 return if op == "==" {
2908 eq_refs
2909 } else {
2910 eq_refs.inverted()
2911 };
2912 }
2913 _ => {}
2914 }
2915 Refinements::empty()
2916 }
2917
2918 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
2920 if let Node::Identifier(var_name) = &object.node {
2921 if let Node::StringLiteral(key) = &args[0].node {
2922 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
2923 if fields.iter().any(|f| f.name == *key && f.optional) {
2924 let narrowed_fields: Vec<ShapeField> = fields
2925 .iter()
2926 .map(|f| {
2927 if f.name == *key {
2928 ShapeField {
2929 name: f.name.clone(),
2930 type_expr: f.type_expr.clone(),
2931 optional: false,
2932 }
2933 } else {
2934 f.clone()
2935 }
2936 })
2937 .collect();
2938 return Refinements {
2939 truthy: vec![(
2940 var_name.clone(),
2941 Some(TypeExpr::Shape(narrowed_fields)),
2942 )],
2943 falsy: vec![],
2944 ..Refinements::default()
2945 };
2946 }
2947 }
2948 }
2949 }
2950 Refinements::empty()
2951 }
2952
2953 fn extract_schema_refinements(args: &[SNode], scope: &TypeScope) -> Refinements {
2954 let Node::Identifier(var_name) = &args[0].node else {
2955 return Refinements::empty();
2956 };
2957 let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) else {
2958 return Refinements::empty();
2959 };
2960 let Some(Some(var_type)) = scope.get_var(var_name).cloned() else {
2961 return Refinements::empty();
2962 };
2963
2964 let truthy = intersect_types(&var_type, &schema_type)
2965 .map(|ty| vec![(var_name.clone(), Some(ty))])
2966 .unwrap_or_default();
2967 let falsy = subtract_type(&var_type, &schema_type)
2968 .map(|ty| vec![(var_name.clone(), Some(ty))])
2969 .unwrap_or_default();
2970
2971 Refinements {
2972 truthy,
2973 falsy,
2974 ..Refinements::default()
2975 }
2976 }
2977
2978 fn block_definitely_exits(stmts: &[SNode]) -> bool {
2980 block_definitely_exits(stmts)
2981 }
2982
2983 fn check_match_exhaustiveness(
2984 &mut self,
2985 value: &SNode,
2986 arms: &[MatchArm],
2987 scope: &TypeScope,
2988 span: Span,
2989 ) {
2990 let enum_name = match &value.node {
2992 Node::PropertyAccess { object, property } if property == "variant" => {
2993 match self.infer_type(object, scope) {
2995 Some(TypeExpr::Named(name)) => {
2996 if scope.get_enum(&name).is_some() {
2997 Some(name)
2998 } else {
2999 None
3000 }
3001 }
3002 _ => None,
3003 }
3004 }
3005 _ => {
3006 match self.infer_type(value, scope) {
3008 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
3009 _ => None,
3010 }
3011 }
3012 };
3013
3014 let Some(enum_name) = enum_name else {
3015 self.check_match_exhaustiveness_union(value, arms, scope, span);
3017 return;
3018 };
3019 let Some(variants) = scope.get_enum(&enum_name) else {
3020 return;
3021 };
3022
3023 let mut covered: Vec<String> = Vec::new();
3025 let mut has_wildcard = false;
3026
3027 for arm in arms {
3028 match &arm.pattern.node {
3029 Node::StringLiteral(s) => covered.push(s.clone()),
3031 Node::Identifier(name)
3033 if name == "_"
3034 || !variants
3035 .variants
3036 .iter()
3037 .any(|variant| variant.name == *name) =>
3038 {
3039 has_wildcard = true;
3040 }
3041 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
3043 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
3045 _ => {
3046 has_wildcard = true;
3048 }
3049 }
3050 }
3051
3052 if has_wildcard {
3053 return;
3054 }
3055
3056 let missing: Vec<&String> = variants
3057 .variants
3058 .iter()
3059 .map(|variant| &variant.name)
3060 .filter(|variant| !covered.contains(variant))
3061 .collect();
3062 if !missing.is_empty() {
3063 let missing_str = missing
3064 .iter()
3065 .map(|s| format!("\"{}\"", s))
3066 .collect::<Vec<_>>()
3067 .join(", ");
3068 self.warning_at(
3069 format!(
3070 "Non-exhaustive match on enum {}: missing variants {}",
3071 enum_name, missing_str
3072 ),
3073 span,
3074 );
3075 }
3076 }
3077
3078 fn check_match_exhaustiveness_union(
3080 &mut self,
3081 value: &SNode,
3082 arms: &[MatchArm],
3083 scope: &TypeScope,
3084 span: Span,
3085 ) {
3086 let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
3087 return;
3088 };
3089 if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
3091 return;
3092 }
3093
3094 let mut has_wildcard = false;
3095 let mut covered_types: Vec<String> = Vec::new();
3096
3097 for arm in arms {
3098 match &arm.pattern.node {
3099 Node::NilLiteral => covered_types.push("nil".into()),
3102 Node::BoolLiteral(_) => {
3103 if !covered_types.contains(&"bool".into()) {
3104 covered_types.push("bool".into());
3105 }
3106 }
3107 Node::IntLiteral(_) => {
3108 if !covered_types.contains(&"int".into()) {
3109 covered_types.push("int".into());
3110 }
3111 }
3112 Node::FloatLiteral(_) => {
3113 if !covered_types.contains(&"float".into()) {
3114 covered_types.push("float".into());
3115 }
3116 }
3117 Node::StringLiteral(_) => {
3118 if !covered_types.contains(&"string".into()) {
3119 covered_types.push("string".into());
3120 }
3121 }
3122 Node::Identifier(name) if name == "_" => {
3123 has_wildcard = true;
3124 }
3125 _ => {
3126 has_wildcard = true;
3127 }
3128 }
3129 }
3130
3131 if has_wildcard {
3132 return;
3133 }
3134
3135 let type_names: Vec<&str> = members
3136 .iter()
3137 .filter_map(|m| match m {
3138 TypeExpr::Named(n) => Some(n.as_str()),
3139 _ => None,
3140 })
3141 .collect();
3142 let missing: Vec<&&str> = type_names
3143 .iter()
3144 .filter(|t| !covered_types.iter().any(|c| c == **t))
3145 .collect();
3146 if !missing.is_empty() {
3147 let missing_str = missing
3148 .iter()
3149 .map(|s| s.to_string())
3150 .collect::<Vec<_>>()
3151 .join(", ");
3152 self.warning_at(
3153 format!(
3154 "Non-exhaustive match on union type: missing {}",
3155 missing_str
3156 ),
3157 span,
3158 );
3159 }
3160 }
3161
3162 const UNKNOWN_CONCRETE_TYPES: &'static [&'static str] = &[
3165 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
3166 ];
3167
3168 fn check_unknown_exhaustiveness(&mut self, scope: &TypeScope, span: Span, site_label: &str) {
3178 let entries = scope.collect_unknown_ruled_out();
3179 for (var_name, covered) in entries {
3180 if covered.is_empty() {
3181 continue;
3182 }
3183 if !matches!(
3186 scope.get_var(&var_name),
3187 Some(Some(TypeExpr::Named(n))) if n == "unknown"
3188 ) {
3189 continue;
3190 }
3191 let missing: Vec<&str> = Self::UNKNOWN_CONCRETE_TYPES
3192 .iter()
3193 .copied()
3194 .filter(|t| !covered.iter().any(|c| c == t))
3195 .collect();
3196 if missing.is_empty() {
3197 continue;
3198 }
3199 let missing_str = missing
3200 .iter()
3201 .map(|s| s.to_string())
3202 .collect::<Vec<_>>()
3203 .join(", ");
3204 self.warning_at(
3205 format!(
3206 "`{site}` reached but `{var}: unknown` was not fully narrowed — uncovered concrete type(s): {missing}",
3207 site = site_label,
3208 var = var_name,
3209 missing = missing_str,
3210 ),
3211 span,
3212 );
3213 }
3214 }
3215
3216 fn visit_for_deprecation(&mut self, node: &SNode) {
3221 match &node.node {
3222 Node::FunctionCall { name, args } => {
3223 if let Some((since, use_hint)) = self.deprecated_fns.get(name).cloned() {
3224 let mut msg = format!("`{name}` is deprecated");
3225 if let Some(s) = since {
3226 msg.push_str(&format!(" (since {s})"));
3227 }
3228 match use_hint {
3229 Some(h) => {
3230 self.warning_at_with_help(msg, node.span, format!("use `{h}` instead"))
3231 }
3232 None => self.warning_at(msg, node.span),
3233 }
3234 }
3235 for a in args {
3236 self.visit_for_deprecation(a);
3237 }
3238 }
3239 Node::MethodCall { object, args, .. }
3240 | Node::OptionalMethodCall { object, args, .. } => {
3241 self.visit_for_deprecation(object);
3242 for a in args {
3243 self.visit_for_deprecation(a);
3244 }
3245 }
3246 Node::AttributedDecl { inner, .. } => self.visit_for_deprecation(inner),
3247 Node::Pipeline { body, .. }
3248 | Node::OverrideDecl { body, .. }
3249 | Node::FnDecl { body, .. }
3250 | Node::ToolDecl { body, .. }
3251 | Node::SpawnExpr { body }
3252 | Node::TryExpr { body }
3253 | Node::Block(body)
3254 | Node::Closure { body, .. }
3255 | Node::WhileLoop { body, .. }
3256 | Node::Retry { body, .. }
3257 | Node::DeferStmt { body }
3258 | Node::MutexBlock { body }
3259 | Node::Parallel { body, .. } => {
3260 for s in body {
3261 self.visit_for_deprecation(s);
3262 }
3263 }
3264 Node::IfElse {
3265 condition,
3266 then_body,
3267 else_body,
3268 } => {
3269 self.visit_for_deprecation(condition);
3270 for s in then_body {
3271 self.visit_for_deprecation(s);
3272 }
3273 if let Some(eb) = else_body {
3274 for s in eb {
3275 self.visit_for_deprecation(s);
3276 }
3277 }
3278 }
3279 Node::ForIn { iterable, body, .. } => {
3280 self.visit_for_deprecation(iterable);
3281 for s in body {
3282 self.visit_for_deprecation(s);
3283 }
3284 }
3285 Node::TryCatch {
3286 body,
3287 catch_body,
3288 finally_body,
3289 ..
3290 } => {
3291 for s in body {
3292 self.visit_for_deprecation(s);
3293 }
3294 for s in catch_body {
3295 self.visit_for_deprecation(s);
3296 }
3297 if let Some(fb) = finally_body {
3298 for s in fb {
3299 self.visit_for_deprecation(s);
3300 }
3301 }
3302 }
3303 Node::DeadlineBlock { duration, body } => {
3304 self.visit_for_deprecation(duration);
3305 for s in body {
3306 self.visit_for_deprecation(s);
3307 }
3308 }
3309 Node::MatchExpr { value, arms } => {
3310 self.visit_for_deprecation(value);
3311 for arm in arms {
3312 if let Some(g) = &arm.guard {
3313 self.visit_for_deprecation(g);
3314 }
3315 for s in &arm.body {
3316 self.visit_for_deprecation(s);
3317 }
3318 }
3319 }
3320 Node::LetBinding { value, .. } | Node::VarBinding { value, .. } => {
3321 self.visit_for_deprecation(value);
3322 }
3323 Node::Assignment { target, value, .. } => {
3324 self.visit_for_deprecation(target);
3325 self.visit_for_deprecation(value);
3326 }
3327 Node::ReturnStmt { value: Some(v) } | Node::YieldExpr { value: Some(v) } => {
3328 self.visit_for_deprecation(v);
3329 }
3330 Node::ThrowStmt { value }
3331 | Node::TryOperator { operand: value }
3332 | Node::TryStar { operand: value }
3333 | Node::Spread(value) => self.visit_for_deprecation(value),
3334 Node::UnaryOp { operand, .. } => self.visit_for_deprecation(operand),
3335 Node::BinaryOp { left, right, .. } => {
3336 self.visit_for_deprecation(left);
3337 self.visit_for_deprecation(right);
3338 }
3339 Node::Ternary {
3340 condition,
3341 true_expr,
3342 false_expr,
3343 } => {
3344 self.visit_for_deprecation(condition);
3345 self.visit_for_deprecation(true_expr);
3346 self.visit_for_deprecation(false_expr);
3347 }
3348 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
3349 self.visit_for_deprecation(object)
3350 }
3351 Node::SubscriptAccess { object, index } => {
3352 self.visit_for_deprecation(object);
3353 self.visit_for_deprecation(index);
3354 }
3355 Node::SliceAccess { object, start, end } => {
3356 self.visit_for_deprecation(object);
3357 if let Some(s) = start {
3358 self.visit_for_deprecation(s);
3359 }
3360 if let Some(e) = end {
3361 self.visit_for_deprecation(e);
3362 }
3363 }
3364 Node::EnumConstruct { args, .. } | Node::ListLiteral(args) => {
3365 for a in args {
3366 self.visit_for_deprecation(a);
3367 }
3368 }
3369 Node::DictLiteral(entries)
3370 | Node::StructConstruct {
3371 fields: entries, ..
3372 } => {
3373 for e in entries {
3374 self.visit_for_deprecation(&e.key);
3375 self.visit_for_deprecation(&e.value);
3376 }
3377 }
3378 Node::GuardStmt {
3379 condition,
3380 else_body,
3381 } => {
3382 self.visit_for_deprecation(condition);
3383 for s in else_body {
3384 self.visit_for_deprecation(s);
3385 }
3386 }
3387 Node::RequireStmt {
3388 condition, message, ..
3389 } => {
3390 self.visit_for_deprecation(condition);
3391 if let Some(m) = message {
3392 self.visit_for_deprecation(m);
3393 }
3394 }
3395 Node::RangeExpr { start, end, .. } => {
3396 self.visit_for_deprecation(start);
3397 self.visit_for_deprecation(end);
3398 }
3399 Node::SelectExpr {
3400 cases,
3401 timeout,
3402 default_body,
3403 } => {
3404 for c in cases {
3405 self.visit_for_deprecation(&c.channel);
3406 for s in &c.body {
3407 self.visit_for_deprecation(s);
3408 }
3409 }
3410 if let Some((d, b)) = timeout {
3411 self.visit_for_deprecation(d);
3412 for s in b {
3413 self.visit_for_deprecation(s);
3414 }
3415 }
3416 if let Some(b) = default_body {
3417 for s in b {
3418 self.visit_for_deprecation(s);
3419 }
3420 }
3421 }
3422 Node::ImplBlock { methods, .. } => {
3423 for m in methods {
3424 self.visit_for_deprecation(m);
3425 }
3426 }
3427 _ => {}
3429 }
3430 }
3431
3432 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
3433 if let Some((since, use_hint)) = self.deprecated_fns.get(name).cloned() {
3438 let mut msg = format!("`{name}` is deprecated");
3439 if let Some(s) = since {
3440 msg.push_str(&format!(" (since {s})"));
3441 }
3442 let help = use_hint.map(|h| format!("use `{h}` instead"));
3443 match help {
3444 Some(h) => self.warning_at_with_help(msg, span, h),
3445 None => self.warning_at(msg, span),
3446 }
3447 }
3448 if name == "unreachable" {
3451 if let Some(arg) = args.first() {
3452 if matches!(&arg.node, Node::Identifier(_)) {
3453 let arg_type = self.infer_type(arg, scope);
3454 if let Some(ref ty) = arg_type {
3455 if !matches!(ty, TypeExpr::Never) {
3456 self.error_at(
3457 format!(
3458 "unreachable() argument has type `{}` — not all cases are handled",
3459 format_type(ty)
3460 ),
3461 span,
3462 );
3463 }
3464 }
3465 }
3466 }
3467 self.check_unknown_exhaustiveness(scope, span, "unreachable()");
3468 for arg in args {
3469 self.check_node(arg, scope);
3470 }
3471 return;
3472 }
3473
3474 if let Some(sig) = scope.get_fn(name).cloned() {
3477 if matches!(sig.return_type, Some(TypeExpr::Never)) {
3478 self.check_unknown_exhaustiveness(scope, span, &format!("{}()", name));
3479 }
3480 }
3481
3482 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
3484 if let Some(sig) = scope.get_fn(name).cloned() {
3485 if !has_spread
3486 && !is_builtin(name)
3487 && !sig.has_rest
3488 && (args.len() < sig.required_params || args.len() > sig.params.len())
3489 {
3490 let expected = if sig.required_params == sig.params.len() {
3491 format!("{}", sig.params.len())
3492 } else {
3493 format!("{}-{}", sig.required_params, sig.params.len())
3494 };
3495 self.warning_at(
3496 format!(
3497 "Function '{}' expects {} arguments, got {}",
3498 name,
3499 expected,
3500 args.len()
3501 ),
3502 span,
3503 );
3504 }
3505 let call_scope = if sig.type_param_names.is_empty() {
3508 scope.clone()
3509 } else {
3510 let mut s = scope.child();
3511 for tp_name in &sig.type_param_names {
3512 s.generic_type_params.insert(tp_name.clone());
3513 }
3514 s
3515 };
3516 let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
3517 let type_param_set: std::collections::BTreeSet<String> =
3518 sig.type_param_names.iter().cloned().collect();
3519 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
3520 if let Some(param_ty) = param_type {
3521 if let Err(message) = self.bind_from_arg_node(
3522 param_ty,
3523 arg,
3524 &type_param_set,
3525 &mut type_bindings,
3526 scope,
3527 ) {
3528 self.error_at(message, arg.span);
3529 }
3530 }
3531 }
3532 for (i, (arg, (param_name, param_type))) in
3533 args.iter().zip(sig.params.iter()).enumerate()
3534 {
3535 if let Some(expected) = param_type {
3536 let actual = self.infer_type(arg, scope);
3537 if let Some(actual) = &actual {
3538 let expected = Self::apply_type_bindings(expected, &type_bindings);
3539 if !self.types_compatible(&expected, actual, &call_scope) {
3540 self.error_at(
3541 format!(
3542 "Argument {} ('{}'): expected {}, got {}",
3543 i + 1,
3544 param_name,
3545 format_type(&expected),
3546 format_type(actual)
3547 ),
3548 arg.span,
3549 );
3550 }
3551 }
3552 }
3553 }
3554 if !sig.where_clauses.is_empty() {
3555 for (type_param, bound) in &sig.where_clauses {
3556 if let Some(concrete_type) = type_bindings.get(type_param) {
3557 let concrete_name = format_type(concrete_type);
3558 let Some(base_type_name) = Self::base_type_name(concrete_type) else {
3559 self.error_at(
3560 format!(
3561 "Type '{}' does not satisfy interface '{}': only named types can satisfy interfaces (required by constraint `where {}: {}`)",
3562 concrete_name, bound, type_param, bound
3563 ),
3564 span,
3565 );
3566 continue;
3567 };
3568 if let Some(reason) = self.interface_mismatch_reason(
3569 base_type_name,
3570 bound,
3571 &BTreeMap::new(),
3572 scope,
3573 ) {
3574 self.error_at(
3575 format!(
3576 "Type '{}' does not satisfy interface '{}': {} \
3577 (required by constraint `where {}: {}`)",
3578 concrete_name, bound, reason, type_param, bound
3579 ),
3580 span,
3581 );
3582 }
3583 }
3584 }
3585 }
3586 }
3587 for arg in args {
3589 self.check_node(arg, scope);
3590 }
3591 }
3592
3593 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
3595 match &snode.node {
3596 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
3597 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
3598 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
3599 Some(TypeExpr::Named("string".into()))
3600 }
3601 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
3602 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
3603 Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
3604 Node::RangeExpr { .. } => Some(TypeExpr::Named("range".into())),
3608 Node::DictLiteral(entries) => {
3609 let mut fields = Vec::new();
3611 for entry in entries {
3612 let key = match &entry.key.node {
3613 Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
3614 _ => return Some(TypeExpr::Named("dict".into())),
3615 };
3616 let val_type = self
3617 .infer_type(&entry.value, scope)
3618 .unwrap_or(TypeExpr::Named("nil".into()));
3619 fields.push(ShapeField {
3620 name: key,
3621 type_expr: val_type,
3622 optional: false,
3623 });
3624 }
3625 if !fields.is_empty() {
3626 Some(TypeExpr::Shape(fields))
3627 } else {
3628 Some(TypeExpr::Named("dict".into()))
3629 }
3630 }
3631 Node::Closure { params, body, .. } => {
3632 let all_typed = params.iter().all(|p| p.type_expr.is_some());
3634 if all_typed && !params.is_empty() {
3635 let param_types: Vec<TypeExpr> =
3636 params.iter().filter_map(|p| p.type_expr.clone()).collect();
3637 let ret = body.last().and_then(|last| self.infer_type(last, scope));
3639 if let Some(ret_type) = ret {
3640 return Some(TypeExpr::FnType {
3641 params: param_types,
3642 return_type: Box::new(ret_type),
3643 });
3644 }
3645 }
3646 Some(TypeExpr::Named("closure".into()))
3647 }
3648
3649 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
3650
3651 Node::FunctionCall { name, args } => {
3652 if let Some(struct_info) = scope.get_struct(name) {
3654 return Some(Self::applied_type_or_name(
3655 name,
3656 struct_info
3657 .type_params
3658 .iter()
3659 .map(|_| Self::wildcard_type())
3660 .collect(),
3661 ));
3662 }
3663 if name == "Ok" {
3664 let ok_type = args
3665 .first()
3666 .and_then(|arg| self.infer_type(arg, scope))
3667 .unwrap_or_else(Self::wildcard_type);
3668 return Some(TypeExpr::Applied {
3669 name: "Result".into(),
3670 args: vec![ok_type, Self::wildcard_type()],
3671 });
3672 }
3673 if name == "Err" {
3674 let err_type = args
3675 .first()
3676 .and_then(|arg| self.infer_type(arg, scope))
3677 .unwrap_or_else(Self::wildcard_type);
3678 return Some(TypeExpr::Applied {
3679 name: "Result".into(),
3680 args: vec![Self::wildcard_type(), err_type],
3681 });
3682 }
3683 if let Some(sig) = scope.get_fn(name).cloned() {
3685 let mut return_type = sig.return_type.clone();
3686 if let Some(ty) = return_type.take() {
3687 if sig.type_param_names.is_empty() {
3688 return Some(ty);
3689 }
3690 let mut bindings = BTreeMap::new();
3691 let type_param_set: std::collections::BTreeSet<String> =
3692 sig.type_param_names.iter().cloned().collect();
3693 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
3694 if let Some(param_ty) = param_type {
3695 let _ = self.bind_from_arg_node(
3696 param_ty,
3697 arg,
3698 &type_param_set,
3699 &mut bindings,
3700 scope,
3701 );
3702 }
3703 }
3704 return Some(Self::apply_type_bindings(&ty, &bindings));
3705 }
3706 return None;
3707 }
3708 if let Some(sig) = builtin_signatures::lookup_generic_builtin_sig(name) {
3715 let type_param_set: std::collections::BTreeSet<String> =
3716 sig.type_params.iter().cloned().collect();
3717 let mut bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
3718 for (arg, param_ty) in args.iter().zip(sig.params.iter()) {
3719 let _ = self.bind_from_arg_node(
3720 param_ty,
3721 arg,
3722 &type_param_set,
3723 &mut bindings,
3724 scope,
3725 );
3726 }
3727 let all_bound = sig.type_params.iter().all(|tp| bindings.contains_key(tp));
3728 if all_bound {
3729 return Some(Self::apply_type_bindings(&sig.return_type, &bindings));
3730 }
3731 }
3732 builtin_return_type(name)
3734 }
3735
3736 Node::BinaryOp { op, left, right } => {
3737 let lt = self.infer_type(left, scope);
3738 let rt = self.infer_type(right, scope);
3739 infer_binary_op_type(op, <, &rt)
3740 }
3741
3742 Node::UnaryOp { op, operand } => {
3743 let t = self.infer_type(operand, scope);
3744 match op.as_str() {
3745 "!" => Some(TypeExpr::Named("bool".into())),
3746 "-" => t, _ => None,
3748 }
3749 }
3750
3751 Node::Ternary {
3752 condition,
3753 true_expr,
3754 false_expr,
3755 } => {
3756 let refs = Self::extract_refinements(condition, scope);
3757
3758 let mut true_scope = scope.child();
3759 refs.apply_truthy(&mut true_scope);
3760 let tt = self.infer_type(true_expr, &true_scope);
3761
3762 let mut false_scope = scope.child();
3763 refs.apply_falsy(&mut false_scope);
3764 let ft = self.infer_type(false_expr, &false_scope);
3765
3766 match (&tt, &ft) {
3767 (Some(a), Some(b)) if a == b => tt,
3768 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
3769 (Some(_), None) => tt,
3770 (None, Some(_)) => ft,
3771 (None, None) => None,
3772 }
3773 }
3774
3775 Node::EnumConstruct {
3776 enum_name,
3777 variant,
3778 args,
3779 } => {
3780 if let Some(enum_info) = scope.get_enum(enum_name) {
3781 Some(self.infer_enum_type(enum_name, enum_info, variant, args, scope))
3782 } else {
3783 Some(TypeExpr::Named(enum_name.clone()))
3784 }
3785 }
3786
3787 Node::PropertyAccess { object, property } => {
3788 if let Node::Identifier(name) = &object.node {
3790 if let Some(enum_info) = scope.get_enum(name) {
3791 return Some(self.infer_enum_type(name, enum_info, property, &[], scope));
3792 }
3793 }
3794 if property == "variant" {
3796 let obj_type = self.infer_type(object, scope);
3797 if let Some(name) = obj_type.as_ref().and_then(Self::base_type_name) {
3798 if scope.get_enum(name).is_some() {
3799 return Some(TypeExpr::Named("string".into()));
3800 }
3801 }
3802 }
3803 let obj_type = self.infer_type(object, scope);
3805 if let Some(TypeExpr::Applied { name, args }) = &obj_type {
3807 if name == "Pair" && args.len() == 2 {
3808 if property == "first" {
3809 return Some(args[0].clone());
3810 } else if property == "second" {
3811 return Some(args[1].clone());
3812 }
3813 }
3814 }
3815 if let Some(TypeExpr::Shape(fields)) = &obj_type {
3816 if let Some(field) = fields.iter().find(|f| f.name == *property) {
3817 return Some(field.type_expr.clone());
3818 }
3819 }
3820 None
3821 }
3822
3823 Node::SubscriptAccess { object, index } => {
3824 let obj_type = self.infer_type(object, scope);
3825 match &obj_type {
3826 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
3827 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
3828 Some(TypeExpr::Shape(fields)) => {
3829 if let Node::StringLiteral(key) = &index.node {
3831 fields
3832 .iter()
3833 .find(|f| &f.name == key)
3834 .map(|f| f.type_expr.clone())
3835 } else {
3836 None
3837 }
3838 }
3839 Some(TypeExpr::Named(n)) if n == "list" => None,
3840 Some(TypeExpr::Named(n)) if n == "dict" => None,
3841 Some(TypeExpr::Named(n)) if n == "string" => {
3842 Some(TypeExpr::Named("string".into()))
3843 }
3844 _ => None,
3845 }
3846 }
3847 Node::SliceAccess { object, .. } => {
3848 let obj_type = self.infer_type(object, scope);
3850 match &obj_type {
3851 Some(TypeExpr::List(_)) => obj_type,
3852 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
3853 Some(TypeExpr::Named(n)) if n == "string" => {
3854 Some(TypeExpr::Named("string".into()))
3855 }
3856 _ => None,
3857 }
3858 }
3859 Node::MethodCall {
3860 object,
3861 method,
3862 args,
3863 }
3864 | Node::OptionalMethodCall {
3865 object,
3866 method,
3867 args,
3868 } => {
3869 if let Node::Identifier(name) = &object.node {
3870 if let Some(enum_info) = scope.get_enum(name) {
3871 return Some(self.infer_enum_type(name, enum_info, method, args, scope));
3872 }
3873 if name == "Result" && (method == "Ok" || method == "Err") {
3874 let ok_type = if method == "Ok" {
3875 args.first()
3876 .and_then(|arg| self.infer_type(arg, scope))
3877 .unwrap_or_else(Self::wildcard_type)
3878 } else {
3879 Self::wildcard_type()
3880 };
3881 let err_type = if method == "Err" {
3882 args.first()
3883 .and_then(|arg| self.infer_type(arg, scope))
3884 .unwrap_or_else(Self::wildcard_type)
3885 } else {
3886 Self::wildcard_type()
3887 };
3888 return Some(TypeExpr::Applied {
3889 name: "Result".into(),
3890 args: vec![ok_type, err_type],
3891 });
3892 }
3893 }
3894 let obj_type = self.infer_type(object, scope);
3895 let iter_elem_type: Option<TypeExpr> = match &obj_type {
3900 Some(TypeExpr::Iter(inner)) => Some((**inner).clone()),
3901 Some(TypeExpr::Named(n)) if n == "iter" => Some(TypeExpr::Named("any".into())),
3902 _ => None,
3903 };
3904 if let Some(t) = iter_elem_type {
3905 let pair = |k: TypeExpr, v: TypeExpr| TypeExpr::Applied {
3906 name: "Pair".into(),
3907 args: vec![k, v],
3908 };
3909 let iter_of = |ty: TypeExpr| TypeExpr::Iter(Box::new(ty));
3910 match method.as_str() {
3911 "iter" => return Some(iter_of(t)),
3912 "map" | "flat_map" => {
3913 return Some(TypeExpr::Named("iter".into()));
3917 }
3918 "filter" | "take" | "skip" | "take_while" | "skip_while" => {
3919 return Some(iter_of(t));
3920 }
3921 "zip" => {
3922 return Some(iter_of(pair(t, TypeExpr::Named("any".into()))));
3923 }
3924 "enumerate" => {
3925 return Some(iter_of(pair(TypeExpr::Named("int".into()), t)));
3926 }
3927 "chain" => return Some(iter_of(t)),
3928 "chunks" | "windows" => {
3929 return Some(iter_of(TypeExpr::List(Box::new(t))));
3930 }
3931 "to_list" => return Some(TypeExpr::List(Box::new(t))),
3933 "to_set" => {
3934 return Some(TypeExpr::Applied {
3935 name: "set".into(),
3936 args: vec![t],
3937 })
3938 }
3939 "to_dict" => return Some(TypeExpr::Named("dict".into())),
3940 "count" => return Some(TypeExpr::Named("int".into())),
3941 "sum" => {
3942 return Some(TypeExpr::Union(vec![
3943 TypeExpr::Named("int".into()),
3944 TypeExpr::Named("float".into()),
3945 ]))
3946 }
3947 "min" | "max" | "first" | "last" | "find" => {
3948 return Some(TypeExpr::Union(vec![t, TypeExpr::Named("nil".into())]));
3949 }
3950 "any" | "all" => return Some(TypeExpr::Named("bool".into())),
3951 "for_each" => return Some(TypeExpr::Named("nil".into())),
3952 "reduce" => return None,
3953 _ => {}
3954 }
3955 }
3956 if method == "iter" {
3961 match &obj_type {
3962 Some(TypeExpr::List(inner)) => {
3963 return Some(TypeExpr::Iter(Box::new((**inner).clone())));
3964 }
3965 Some(TypeExpr::DictType(k, v)) => {
3966 return Some(TypeExpr::Iter(Box::new(TypeExpr::Applied {
3967 name: "Pair".into(),
3968 args: vec![(**k).clone(), (**v).clone()],
3969 })));
3970 }
3971 Some(TypeExpr::Named(n))
3972 if n == "list" || n == "dict" || n == "set" || n == "string" =>
3973 {
3974 return Some(TypeExpr::Named("iter".into()));
3975 }
3976 _ => {}
3977 }
3978 }
3979 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
3980 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
3981 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
3982 match method.as_str() {
3983 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
3985 Some(TypeExpr::Named("bool".into()))
3986 }
3987 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
3989 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
3991 | "pad_left" | "pad_right" | "repeat" | "join" => {
3992 Some(TypeExpr::Named("string".into()))
3993 }
3994 "split" | "chars" => Some(TypeExpr::Named("list".into())),
3995 "filter" => {
3997 if is_dict {
3998 Some(TypeExpr::Named("dict".into()))
3999 } else {
4000 Some(TypeExpr::Named("list".into()))
4001 }
4002 }
4003 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
4005 "window" | "each_cons" | "sliding_window" => match &obj_type {
4006 Some(TypeExpr::List(inner)) => Some(TypeExpr::List(Box::new(
4007 TypeExpr::List(Box::new((**inner).clone())),
4008 ))),
4009 _ => Some(TypeExpr::Named("list".into())),
4010 },
4011 "reduce" | "find" | "first" | "last" => None,
4012 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
4014 "merge" | "map_values" | "rekey" | "map_keys" => {
4015 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
4019 Some(TypeExpr::DictType(
4020 Box::new(TypeExpr::Named("string".into())),
4021 v.clone(),
4022 ))
4023 } else {
4024 Some(TypeExpr::Named("dict".into()))
4025 }
4026 }
4027 "to_string" => Some(TypeExpr::Named("string".into())),
4029 "to_int" => Some(TypeExpr::Named("int".into())),
4030 "to_float" => Some(TypeExpr::Named("float".into())),
4031 _ => None,
4032 }
4033 }
4034
4035 Node::TryOperator { operand } => match self.infer_type(operand, scope) {
4037 Some(TypeExpr::Applied { name, args }) if name == "Result" && args.len() == 2 => {
4038 Some(args[0].clone())
4039 }
4040 Some(TypeExpr::Named(name)) if name == "Result" => None,
4041 _ => None,
4042 },
4043
4044 Node::ThrowStmt { .. }
4046 | Node::ReturnStmt { .. }
4047 | Node::BreakStmt
4048 | Node::ContinueStmt => Some(TypeExpr::Never),
4049
4050 Node::IfElse {
4052 then_body,
4053 else_body,
4054 ..
4055 } => {
4056 let then_type = self.infer_block_type(then_body, scope);
4057 let else_type = else_body
4058 .as_ref()
4059 .and_then(|eb| self.infer_block_type(eb, scope));
4060 match (then_type, else_type) {
4061 (Some(TypeExpr::Never), Some(TypeExpr::Never)) => Some(TypeExpr::Never),
4062 (Some(TypeExpr::Never), Some(other)) | (Some(other), Some(TypeExpr::Never)) => {
4063 Some(other)
4064 }
4065 (Some(t), Some(e)) if t == e => Some(t),
4066 (Some(t), Some(e)) => Some(simplify_union(vec![t, e])),
4067 (Some(t), None) => Some(t),
4068 (None, _) => None,
4069 }
4070 }
4071
4072 Node::TryExpr { body } => {
4073 let ok_type = self
4074 .infer_block_type(body, scope)
4075 .unwrap_or_else(Self::wildcard_type);
4076 let err_type = self
4077 .infer_try_error_type(body, scope)
4078 .unwrap_or_else(Self::wildcard_type);
4079 Some(TypeExpr::Applied {
4080 name: "Result".into(),
4081 args: vec![ok_type, err_type],
4082 })
4083 }
4084
4085 Node::TryStar { operand } => self.infer_type(operand, scope),
4088
4089 Node::StructConstruct {
4090 struct_name,
4091 fields,
4092 } => scope
4093 .get_struct(struct_name)
4094 .map(|struct_info| self.infer_struct_type(struct_name, struct_info, fields, scope))
4095 .or_else(|| Some(TypeExpr::Named(struct_name.clone()))),
4096
4097 _ => None,
4098 }
4099 }
4100
4101 fn infer_block_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
4103 if Self::block_definitely_exits(stmts) {
4104 return Some(TypeExpr::Never);
4105 }
4106 stmts.last().and_then(|s| self.infer_type(s, scope))
4107 }
4108
4109 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
4116 self.types_compatible_at(Polarity::Covariant, expected, actual, scope)
4117 }
4118
4119 fn types_compatible_at(
4129 &self,
4130 polarity: Polarity,
4131 expected: &TypeExpr,
4132 actual: &TypeExpr,
4133 scope: &TypeScope,
4134 ) -> bool {
4135 match polarity {
4136 Polarity::Covariant => {}
4137 Polarity::Contravariant => {
4138 return self.types_compatible_at(Polarity::Covariant, actual, expected, scope);
4139 }
4140 Polarity::Invariant => {
4141 return self.types_compatible_at(Polarity::Covariant, expected, actual, scope)
4142 && self.types_compatible_at(Polarity::Covariant, actual, expected, scope);
4143 }
4144 }
4145
4146 if Self::is_wildcard_type(expected) || Self::is_wildcard_type(actual) {
4148 return true;
4149 }
4150 if let TypeExpr::Named(name) = expected {
4152 if scope.is_generic_type_param(name) {
4153 return true;
4154 }
4155 }
4156 if let TypeExpr::Named(name) = actual {
4157 if scope.is_generic_type_param(name) {
4158 return true;
4159 }
4160 }
4161 let expected = self.resolve_alias(expected, scope);
4162 let actual = self.resolve_alias(actual, scope);
4163
4164 if let Some(iface_name) = Self::base_type_name(&expected) {
4166 if let Some(interface_info) = scope.get_interface(iface_name) {
4167 let mut interface_bindings = BTreeMap::new();
4168 if let TypeExpr::Applied { args, .. } = &expected {
4169 for (type_param, arg) in interface_info.type_params.iter().zip(args.iter()) {
4170 interface_bindings.insert(type_param.name.clone(), arg.clone());
4171 }
4172 }
4173 if let Some(type_name) = Self::base_type_name(&actual) {
4174 return self.satisfies_interface(
4175 type_name,
4176 iface_name,
4177 &interface_bindings,
4178 scope,
4179 );
4180 }
4181 return false;
4182 }
4183 }
4184
4185 match (&expected, &actual) {
4186 (_, TypeExpr::Never) => true,
4188 (TypeExpr::Never, _) => false,
4190 (TypeExpr::Named(n), _) if n == "any" => true,
4193 (_, TypeExpr::Named(n)) if n == "any" => true,
4194 (TypeExpr::Named(n), _) if n == "unknown" => true,
4198 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
4202 (TypeExpr::Named(a), TypeExpr::Applied { name: b, .. })
4203 | (TypeExpr::Applied { name: a, .. }, TypeExpr::Named(b)) => a == b,
4204 (
4205 TypeExpr::Applied {
4206 name: expected_name,
4207 args: expected_args,
4208 },
4209 TypeExpr::Applied {
4210 name: actual_name,
4211 args: actual_args,
4212 },
4213 ) => {
4214 if expected_name != actual_name || expected_args.len() != actual_args.len() {
4215 return false;
4216 }
4217 let variances = scope.variance_of(expected_name);
4227 for (idx, (expected_arg, actual_arg)) in
4228 expected_args.iter().zip(actual_args.iter()).enumerate()
4229 {
4230 let child_variance = variances
4231 .as_ref()
4232 .and_then(|v| v.get(idx).copied())
4233 .unwrap_or(Variance::Invariant);
4234 let arg_polarity = Polarity::Covariant.compose(child_variance);
4235 if !self.types_compatible_at(arg_polarity, expected_arg, actual_arg, scope) {
4236 return false;
4237 }
4238 }
4239 true
4240 }
4241 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
4244 act_members.iter().all(|am| {
4245 exp_members
4246 .iter()
4247 .any(|em| self.types_compatible(em, am, scope))
4248 })
4249 }
4250 (TypeExpr::Union(members), actual_type) => members
4251 .iter()
4252 .any(|m| self.types_compatible(m, actual_type, scope)),
4253 (expected_type, TypeExpr::Union(members)) => members
4254 .iter()
4255 .all(|m| self.types_compatible(expected_type, m, scope)),
4256 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
4257 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
4258 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
4259 if expected_field.optional {
4260 return true;
4261 }
4262 af.iter().any(|actual_field| {
4263 actual_field.name == expected_field.name
4264 && self.types_compatible(
4265 &expected_field.type_expr,
4266 &actual_field.type_expr,
4267 scope,
4268 )
4269 })
4270 }),
4271 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
4273 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
4274 keys_ok
4275 && af
4276 .iter()
4277 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
4278 }
4279 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
4281 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
4288 self.types_compatible_at(Polarity::Invariant, expected_inner, actual_inner, scope)
4289 }
4290 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
4291 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
4292 (TypeExpr::Iter(expected_inner), TypeExpr::Iter(actual_inner)) => {
4296 self.types_compatible(expected_inner, actual_inner, scope)
4297 }
4298 (TypeExpr::Named(n), TypeExpr::Iter(_)) if n == "iter" => true,
4299 (TypeExpr::Iter(_), TypeExpr::Named(n)) if n == "iter" => true,
4300 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
4304 self.types_compatible_at(Polarity::Invariant, ek, ak, scope)
4305 && self.types_compatible_at(Polarity::Invariant, ev, av, scope)
4306 }
4307 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
4308 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
4309 (
4316 TypeExpr::FnType {
4317 params: ep,
4318 return_type: er,
4319 },
4320 TypeExpr::FnType {
4321 params: ap,
4322 return_type: ar,
4323 },
4324 ) => {
4325 ep.len() == ap.len()
4326 && ep.iter().zip(ap.iter()).all(|(e, a)| {
4327 self.types_compatible_at(Polarity::Contravariant, e, a, scope)
4328 })
4329 && self.types_compatible(er, ar, scope)
4330 }
4331 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
4333 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
4334 (TypeExpr::LitString(a), TypeExpr::LitString(b)) => a == b,
4343 (TypeExpr::LitInt(a), TypeExpr::LitInt(b)) => a == b,
4344 (TypeExpr::Named(n), TypeExpr::LitString(_)) if n == "string" => true,
4345 (TypeExpr::Named(n), TypeExpr::LitInt(_)) if n == "int" || n == "float" => true,
4346 (TypeExpr::LitString(_), TypeExpr::Named(n)) if n == "string" => true,
4347 (TypeExpr::LitInt(_), TypeExpr::Named(n)) if n == "int" => true,
4348 _ => false,
4349 }
4350 }
4351
4352 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
4353 match ty {
4354 TypeExpr::Named(name) => {
4355 if let Some(resolved) = scope.resolve_type(name) {
4356 return self.resolve_alias(resolved, scope);
4357 }
4358 ty.clone()
4359 }
4360 TypeExpr::Union(types) => TypeExpr::Union(
4361 types
4362 .iter()
4363 .map(|ty| self.resolve_alias(ty, scope))
4364 .collect(),
4365 ),
4366 TypeExpr::Shape(fields) => TypeExpr::Shape(
4367 fields
4368 .iter()
4369 .map(|field| ShapeField {
4370 name: field.name.clone(),
4371 type_expr: self.resolve_alias(&field.type_expr, scope),
4372 optional: field.optional,
4373 })
4374 .collect(),
4375 ),
4376 TypeExpr::List(inner) => TypeExpr::List(Box::new(self.resolve_alias(inner, scope))),
4377 TypeExpr::Iter(inner) => TypeExpr::Iter(Box::new(self.resolve_alias(inner, scope))),
4378 TypeExpr::DictType(key, value) => TypeExpr::DictType(
4379 Box::new(self.resolve_alias(key, scope)),
4380 Box::new(self.resolve_alias(value, scope)),
4381 ),
4382 TypeExpr::FnType {
4383 params,
4384 return_type,
4385 } => TypeExpr::FnType {
4386 params: params
4387 .iter()
4388 .map(|param| self.resolve_alias(param, scope))
4389 .collect(),
4390 return_type: Box::new(self.resolve_alias(return_type, scope)),
4391 },
4392 TypeExpr::Applied { name, args } => TypeExpr::Applied {
4393 name: name.clone(),
4394 args: args
4395 .iter()
4396 .map(|arg| self.resolve_alias(arg, scope))
4397 .collect(),
4398 },
4399 TypeExpr::Never => TypeExpr::Never,
4400 TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
4401 TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
4402 }
4403 }
4404
4405 fn check_fn_decl_variance(
4409 &mut self,
4410 type_params: &[TypeParam],
4411 params: &[TypedParam],
4412 return_type: Option<&TypeExpr>,
4413 name: &str,
4414 span: Span,
4415 ) {
4416 let mut positions: Vec<(&TypeExpr, Polarity)> = Vec::new();
4417 for p in params {
4418 if let Some(te) = &p.type_expr {
4419 positions.push((te, Polarity::Contravariant));
4420 }
4421 }
4422 if let Some(rt) = return_type {
4423 positions.push((rt, Polarity::Covariant));
4424 }
4425 let kind = format!("function '{name}'");
4426 self.check_decl_variance(&kind, type_params, &positions, span);
4427 }
4428
4429 fn check_type_alias_decl_variance(
4432 &mut self,
4433 type_params: &[TypeParam],
4434 type_expr: &TypeExpr,
4435 name: &str,
4436 span: Span,
4437 ) {
4438 let positions = [(type_expr, Polarity::Covariant)];
4439 let kind = format!("type alias '{name}'");
4440 self.check_decl_variance(&kind, type_params, &positions, span);
4441 }
4442
4443 fn check_enum_decl_variance(
4447 &mut self,
4448 type_params: &[TypeParam],
4449 variants: &[EnumVariant],
4450 name: &str,
4451 span: Span,
4452 ) {
4453 let mut positions: Vec<(&TypeExpr, Polarity)> = Vec::new();
4454 for variant in variants {
4455 for field in &variant.fields {
4456 if let Some(te) = &field.type_expr {
4457 positions.push((te, Polarity::Covariant));
4458 }
4459 }
4460 }
4461 let kind = format!("enum '{name}'");
4462 self.check_decl_variance(&kind, type_params, &positions, span);
4463 }
4464
4465 fn check_struct_decl_variance(
4470 &mut self,
4471 type_params: &[TypeParam],
4472 fields: &[StructField],
4473 name: &str,
4474 span: Span,
4475 ) {
4476 let positions: Vec<(&TypeExpr, Polarity)> = fields
4477 .iter()
4478 .filter_map(|f| f.type_expr.as_ref().map(|te| (te, Polarity::Invariant)))
4479 .collect();
4480 let kind = format!("struct '{name}'");
4481 self.check_decl_variance(&kind, type_params, &positions, span);
4482 }
4483
4484 fn check_interface_decl_variance(
4488 &mut self,
4489 type_params: &[TypeParam],
4490 methods: &[InterfaceMethod],
4491 name: &str,
4492 span: Span,
4493 ) {
4494 let mut positions: Vec<(&TypeExpr, Polarity)> = Vec::new();
4495 for method in methods {
4496 for p in &method.params {
4497 if let Some(te) = &p.type_expr {
4498 positions.push((te, Polarity::Contravariant));
4499 }
4500 }
4501 if let Some(rt) = &method.return_type {
4502 positions.push((rt, Polarity::Covariant));
4503 }
4504 }
4505 let kind = format!("interface '{name}'");
4506 self.check_decl_variance(&kind, type_params, &positions, span);
4507 }
4508
4509 fn check_decl_variance(
4524 &mut self,
4525 decl_kind: &str,
4526 type_params: &[TypeParam],
4527 positions: &[(&TypeExpr, Polarity)],
4528 span: Span,
4529 ) {
4530 if type_params
4534 .iter()
4535 .all(|tp| tp.variance == Variance::Invariant)
4536 {
4537 return;
4538 }
4539 let declared: BTreeMap<String, Variance> = type_params
4540 .iter()
4541 .map(|tp| (tp.name.clone(), tp.variance))
4542 .collect();
4543 for (ty, polarity) in positions {
4544 self.walk_variance(decl_kind, ty, *polarity, &declared, span);
4545 }
4546 }
4547
4548 #[allow(clippy::only_used_in_recursion)]
4550 fn walk_variance(
4551 &mut self,
4552 decl_kind: &str,
4553 ty: &TypeExpr,
4554 polarity: Polarity,
4555 declared: &BTreeMap<String, Variance>,
4556 span: Span,
4557 ) {
4558 match ty {
4559 TypeExpr::Named(name) => {
4560 if let Some(&declared_variance) = declared.get(name) {
4561 let ok = matches!(
4562 (declared_variance, polarity),
4563 (Variance::Invariant, _)
4564 | (Variance::Covariant, Polarity::Covariant)
4565 | (Variance::Contravariant, Polarity::Contravariant)
4566 );
4567 if !ok {
4568 let (marker, declared_word) = match declared_variance {
4569 Variance::Covariant => ("out", "covariant"),
4570 Variance::Contravariant => ("in", "contravariant"),
4571 Variance::Invariant => unreachable!(),
4572 };
4573 let position_word = match polarity {
4574 Polarity::Covariant => "covariant",
4575 Polarity::Contravariant => "contravariant",
4576 Polarity::Invariant => "invariant",
4577 };
4578 self.error_at(
4579 format!(
4580 "type parameter '{name}' is declared '{marker}' \
4581 ({declared_word}) but appears in a \
4582 {position_word} position in {decl_kind}"
4583 ),
4584 span,
4585 );
4586 }
4587 }
4588 }
4589 TypeExpr::List(inner) | TypeExpr::Iter(inner) => {
4590 let sub = match ty {
4592 TypeExpr::List(_) => Polarity::Invariant,
4593 TypeExpr::Iter(_) => polarity,
4594 _ => unreachable!(),
4595 };
4596 self.walk_variance(decl_kind, inner, sub, declared, span);
4597 }
4598 TypeExpr::DictType(k, v) => {
4599 self.walk_variance(decl_kind, k, Polarity::Invariant, declared, span);
4601 self.walk_variance(decl_kind, v, Polarity::Invariant, declared, span);
4602 }
4603 TypeExpr::Shape(fields) => {
4604 for f in fields {
4605 self.walk_variance(decl_kind, &f.type_expr, polarity, declared, span);
4606 }
4607 }
4608 TypeExpr::Union(members) => {
4609 for m in members {
4610 self.walk_variance(decl_kind, m, polarity, declared, span);
4611 }
4612 }
4613 TypeExpr::FnType {
4614 params,
4615 return_type,
4616 } => {
4617 let param_polarity = polarity.compose(Variance::Contravariant);
4620 for p in params {
4621 self.walk_variance(decl_kind, p, param_polarity, declared, span);
4622 }
4623 self.walk_variance(decl_kind, return_type, polarity, declared, span);
4624 }
4625 TypeExpr::Applied { name, args } => {
4626 let variances: Option<Vec<Variance>> = self
4631 .scope
4632 .get_enum(name)
4633 .map(|info| info.type_params.iter().map(|tp| tp.variance).collect())
4634 .or_else(|| {
4635 self.scope
4636 .get_struct(name)
4637 .map(|info| info.type_params.iter().map(|tp| tp.variance).collect())
4638 })
4639 .or_else(|| {
4640 self.scope
4641 .get_interface(name)
4642 .map(|info| info.type_params.iter().map(|tp| tp.variance).collect())
4643 });
4644 for (idx, arg) in args.iter().enumerate() {
4645 let child_variance = variances
4646 .as_ref()
4647 .and_then(|v| v.get(idx).copied())
4648 .unwrap_or(Variance::Invariant);
4649 let sub = polarity.compose(child_variance);
4650 self.walk_variance(decl_kind, arg, sub, declared, span);
4651 }
4652 }
4653 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => {}
4654 }
4655 }
4656
4657 fn error_at(&mut self, message: String, span: Span) {
4658 self.diagnostics.push(TypeDiagnostic {
4659 message,
4660 severity: DiagnosticSeverity::Error,
4661 span: Some(span),
4662 help: None,
4663 fix: None,
4664 });
4665 }
4666
4667 #[allow(dead_code)]
4668 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
4669 self.diagnostics.push(TypeDiagnostic {
4670 message,
4671 severity: DiagnosticSeverity::Error,
4672 span: Some(span),
4673 help: Some(help),
4674 fix: None,
4675 });
4676 }
4677
4678 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
4679 self.diagnostics.push(TypeDiagnostic {
4680 message,
4681 severity: DiagnosticSeverity::Error,
4682 span: Some(span),
4683 help: None,
4684 fix: Some(fix),
4685 });
4686 }
4687
4688 fn warning_at(&mut self, message: String, span: Span) {
4689 self.diagnostics.push(TypeDiagnostic {
4690 message,
4691 severity: DiagnosticSeverity::Warning,
4692 span: Some(span),
4693 help: None,
4694 fix: None,
4695 });
4696 }
4697
4698 #[allow(dead_code)]
4699 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
4700 self.diagnostics.push(TypeDiagnostic {
4701 message,
4702 severity: DiagnosticSeverity::Warning,
4703 span: Some(span),
4704 help: Some(help),
4705 fix: None,
4706 });
4707 }
4708
4709 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
4713 match &snode.node {
4714 Node::BinaryOp { op, left, right } => {
4715 self.check_binops(left, scope);
4716 self.check_binops(right, scope);
4717 let lt = self.infer_type(left, scope);
4718 let rt = self.infer_type(right, scope);
4719 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
4720 let span = snode.span;
4721 match op.as_str() {
4722 "+" => {
4723 let valid = matches!(
4724 (l.as_str(), r.as_str()),
4725 ("int" | "float", "int" | "float")
4726 | ("string", "string")
4727 | ("list", "list")
4728 | ("dict", "dict")
4729 );
4730 if !valid {
4731 let msg = format!("can't add {} and {}", l, r);
4732 let fix = if l == "string" || r == "string" {
4733 self.build_interpolation_fix(left, right, l == "string", span)
4734 } else {
4735 None
4736 };
4737 if let Some(fix) = fix {
4738 self.error_at_with_fix(msg, span, fix);
4739 } else {
4740 self.error_at(msg, span);
4741 }
4742 }
4743 }
4744 "-" | "/" | "%" | "**" => {
4745 let numeric = ["int", "float"];
4746 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
4747 self.error_at(
4748 format!(
4749 "can't use '{}' on {} and {} (needs numeric operands)",
4750 op, l, r
4751 ),
4752 span,
4753 );
4754 }
4755 }
4756 "*" => {
4757 let numeric = ["int", "float"];
4758 let is_numeric =
4759 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
4760 let is_string_repeat =
4761 (l == "string" && r == "int") || (l == "int" && r == "string");
4762 if !is_numeric && !is_string_repeat {
4763 self.error_at(
4764 format!("can't multiply {} and {} (try string * int)", l, r),
4765 span,
4766 );
4767 }
4768 }
4769 _ => {}
4770 }
4771 }
4772 }
4773 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
4775 _ => {}
4776 }
4777 }
4778
4779 fn build_interpolation_fix(
4781 &self,
4782 left: &SNode,
4783 right: &SNode,
4784 left_is_string: bool,
4785 expr_span: Span,
4786 ) -> Option<Vec<FixEdit>> {
4787 let src = self.source.as_ref()?;
4788 let (str_node, other_node) = if left_is_string {
4789 (left, right)
4790 } else {
4791 (right, left)
4792 };
4793 let str_text = src.get(str_node.span.start..str_node.span.end)?;
4794 let other_text = src.get(other_node.span.start..other_node.span.end)?;
4795 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
4797 if other_text.contains('}') || other_text.contains('"') {
4799 return None;
4800 }
4801 let replacement = if left_is_string {
4802 format!("\"{inner}${{{other_text}}}\"")
4803 } else {
4804 format!("\"${{{other_text}}}{inner}\"")
4805 };
4806 Some(vec![FixEdit {
4807 span: expr_span,
4808 replacement,
4809 }])
4810 }
4811}
4812
4813impl Default for TypeChecker {
4814 fn default() -> Self {
4815 Self::new()
4816 }
4817}
4818
4819fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
4821 match op {
4822 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
4823 Some(TypeExpr::Named("bool".into()))
4824 }
4825 "+" => match (left, right) {
4826 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
4827 match (l.as_str(), r.as_str()) {
4828 ("int", "int") => Some(TypeExpr::Named("int".into())),
4829 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
4830 ("string", "string") => Some(TypeExpr::Named("string".into())),
4831 ("list", "list") => Some(TypeExpr::Named("list".into())),
4832 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
4833 _ => None,
4834 }
4835 }
4836 _ => None,
4837 },
4838 "-" | "/" | "%" => match (left, right) {
4839 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
4840 match (l.as_str(), r.as_str()) {
4841 ("int", "int") => Some(TypeExpr::Named("int".into())),
4842 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
4843 _ => None,
4844 }
4845 }
4846 _ => None,
4847 },
4848 "**" => match (left, right) {
4849 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
4850 match (l.as_str(), r.as_str()) {
4851 ("int", "int") => Some(TypeExpr::Named("int".into())),
4852 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
4853 _ => None,
4854 }
4855 }
4856 _ => None,
4857 },
4858 "*" => match (left, right) {
4859 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
4860 match (l.as_str(), r.as_str()) {
4861 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
4862 ("int", "int") => Some(TypeExpr::Named("int".into())),
4863 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
4864 _ => None,
4865 }
4866 }
4867 _ => None,
4868 },
4869 "??" => match (left, right) {
4870 (Some(TypeExpr::Union(members)), _) => {
4872 let non_nil: Vec<_> = members
4873 .iter()
4874 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
4875 .cloned()
4876 .collect();
4877 if non_nil.len() == 1 {
4878 Some(non_nil[0].clone())
4879 } else if non_nil.is_empty() {
4880 right.clone()
4881 } else {
4882 Some(TypeExpr::Union(non_nil))
4883 }
4884 }
4885 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
4887 (Some(l), _) => Some(l.clone()),
4889 (None, _) => right.clone(),
4891 },
4892 "|>" => None,
4893 _ => None,
4894 }
4895}
4896
4897pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
4902 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
4903 let mut details = Vec::new();
4904 for field in ef {
4905 if field.optional {
4906 continue;
4907 }
4908 match af.iter().find(|f| f.name == field.name) {
4909 None => details.push(format!(
4910 "missing field '{}' ({})",
4911 field.name,
4912 format_type(&field.type_expr)
4913 )),
4914 Some(actual_field) => {
4915 let e_str = format_type(&field.type_expr);
4916 let a_str = format_type(&actual_field.type_expr);
4917 if e_str != a_str {
4918 details.push(format!(
4919 "field '{}' has type {}, expected {}",
4920 field.name, a_str, e_str
4921 ));
4922 }
4923 }
4924 }
4925 }
4926 if details.is_empty() {
4927 None
4928 } else {
4929 Some(details.join("; "))
4930 }
4931 } else {
4932 None
4933 }
4934}
4935
4936fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
4939 matches!(
4940 &value.node,
4941 Node::IntLiteral(_)
4942 | Node::FloatLiteral(_)
4943 | Node::StringLiteral(_)
4944 | Node::BoolLiteral(_)
4945 | Node::NilLiteral
4946 | Node::ListLiteral(_)
4947 | Node::DictLiteral(_)
4948 | Node::InterpolatedString(_)
4949 )
4950}
4951
4952pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
4955 match &stmt.node {
4956 Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
4957 true
4958 }
4959 Node::IfElse {
4960 then_body,
4961 else_body: Some(else_body),
4962 ..
4963 } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
4964 _ => false,
4965 }
4966}
4967
4968pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
4970 stmts.iter().any(stmt_definitely_exits)
4971}
4972
4973pub fn format_type(ty: &TypeExpr) -> String {
4974 match ty {
4975 TypeExpr::Named(n) => n.clone(),
4976 TypeExpr::Union(types) => types
4977 .iter()
4978 .map(format_type)
4979 .collect::<Vec<_>>()
4980 .join(" | "),
4981 TypeExpr::Shape(fields) => {
4982 let inner: Vec<String> = fields
4983 .iter()
4984 .map(|f| {
4985 let opt = if f.optional { "?" } else { "" };
4986 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
4987 })
4988 .collect();
4989 format!("{{{}}}", inner.join(", "))
4990 }
4991 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
4992 TypeExpr::Iter(inner) => format!("iter<{}>", format_type(inner)),
4993 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
4994 TypeExpr::Applied { name, args } => {
4995 let args_str = args.iter().map(format_type).collect::<Vec<_>>().join(", ");
4996 format!("{name}<{args_str}>")
4997 }
4998 TypeExpr::FnType {
4999 params,
5000 return_type,
5001 } => {
5002 let params_str = params
5003 .iter()
5004 .map(format_type)
5005 .collect::<Vec<_>>()
5006 .join(", ");
5007 format!("fn({}) -> {}", params_str, format_type(return_type))
5008 }
5009 TypeExpr::Never => "never".to_string(),
5010 TypeExpr::LitString(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
5011 TypeExpr::LitInt(v) => v.to_string(),
5012 }
5013}
5014
5015fn simplify_union(members: Vec<TypeExpr>) -> TypeExpr {
5017 let filtered: Vec<TypeExpr> = members
5018 .into_iter()
5019 .filter(|m| !matches!(m, TypeExpr::Never))
5020 .collect();
5021 match filtered.len() {
5022 0 => TypeExpr::Never,
5023 1 => filtered.into_iter().next().unwrap(),
5024 _ => TypeExpr::Union(filtered),
5025 }
5026}
5027
5028fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
5031 let remaining: Vec<TypeExpr> = members
5032 .iter()
5033 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
5034 .cloned()
5035 .collect();
5036 match remaining.len() {
5037 0 => Some(TypeExpr::Never),
5038 1 => Some(remaining.into_iter().next().unwrap()),
5039 _ => Some(TypeExpr::Union(remaining)),
5040 }
5041}
5042
5043fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
5045 if members
5046 .iter()
5047 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
5048 {
5049 Some(TypeExpr::Named(target.to_string()))
5050 } else {
5051 None
5052 }
5053}
5054
5055fn extract_type_of_var(node: &SNode) -> Option<String> {
5057 if let Node::FunctionCall { name, args } = &node.node {
5058 if name == "type_of" && args.len() == 1 {
5059 if let Node::Identifier(var) = &args[0].node {
5060 return Some(var.clone());
5061 }
5062 }
5063 }
5064 None
5065}
5066
5067fn schema_type_expr_from_node(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
5068 match &node.node {
5069 Node::Identifier(name) => {
5070 if let Some(schema) = scope.get_schema_binding(name).cloned().flatten() {
5075 return Some(schema);
5076 }
5077 scope.resolve_type(name).cloned()
5078 }
5079 Node::DictLiteral(entries) => schema_type_expr_from_dict(entries, scope),
5080 Node::FunctionCall { name, args } if name == "schema_of" && args.len() == 1 => {
5085 if let Node::Identifier(alias) = &args[0].node {
5086 return scope.resolve_type(alias).cloned();
5087 }
5088 None
5089 }
5090 _ => None,
5091 }
5092}
5093
5094fn schema_type_expr_from_dict(entries: &[DictEntry], scope: &TypeScope) -> Option<TypeExpr> {
5095 let mut type_name: Option<String> = None;
5096 let mut properties: Option<&SNode> = None;
5097 let mut required: Option<Vec<String>> = None;
5098 let mut items: Option<&SNode> = None;
5099 let mut union: Option<&SNode> = None;
5100 let mut nullable = false;
5101 let mut additional_properties: Option<&SNode> = None;
5102
5103 for entry in entries {
5104 let key = schema_entry_key(&entry.key)?;
5105 match key.as_str() {
5106 "type" => match &entry.value.node {
5107 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
5108 type_name = Some(normalize_schema_type_name(text));
5109 }
5110 Node::ListLiteral(items_list) => {
5111 let union_members = items_list
5112 .iter()
5113 .filter_map(|item| match &item.node {
5114 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
5115 Some(TypeExpr::Named(normalize_schema_type_name(text)))
5116 }
5117 _ => None,
5118 })
5119 .collect::<Vec<_>>();
5120 if !union_members.is_empty() {
5121 return Some(TypeExpr::Union(union_members));
5122 }
5123 }
5124 _ => {}
5125 },
5126 "properties" => properties = Some(&entry.value),
5127 "required" => {
5128 required = schema_required_names(&entry.value);
5129 }
5130 "items" => items = Some(&entry.value),
5131 "union" | "oneOf" | "anyOf" => union = Some(&entry.value),
5132 "nullable" => {
5133 nullable = matches!(entry.value.node, Node::BoolLiteral(true));
5134 }
5135 "additional_properties" | "additionalProperties" => {
5136 additional_properties = Some(&entry.value);
5137 }
5138 _ => {}
5139 }
5140 }
5141
5142 let mut schema_type = if let Some(union_node) = union {
5143 schema_union_type_expr(union_node, scope)?
5144 } else if let Some(properties_node) = properties {
5145 let property_entries = match &properties_node.node {
5146 Node::DictLiteral(entries) => entries,
5147 _ => return None,
5148 };
5149 let required_names = required.unwrap_or_default();
5150 let mut fields = Vec::new();
5151 for entry in property_entries {
5152 let field_name = schema_entry_key(&entry.key)?;
5153 let field_type = schema_type_expr_from_node(&entry.value, scope)?;
5154 fields.push(ShapeField {
5155 name: field_name.clone(),
5156 type_expr: field_type,
5157 optional: !required_names.contains(&field_name),
5158 });
5159 }
5160 TypeExpr::Shape(fields)
5161 } else if let Some(item_node) = items {
5162 TypeExpr::List(Box::new(schema_type_expr_from_node(item_node, scope)?))
5163 } else if let Some(type_name) = type_name {
5164 if type_name == "dict" {
5165 if let Some(extra_node) = additional_properties {
5166 let value_type = match &extra_node.node {
5167 Node::BoolLiteral(_) => None,
5168 _ => schema_type_expr_from_node(extra_node, scope),
5169 };
5170 if let Some(value_type) = value_type {
5171 TypeExpr::DictType(
5172 Box::new(TypeExpr::Named("string".into())),
5173 Box::new(value_type),
5174 )
5175 } else {
5176 TypeExpr::Named(type_name)
5177 }
5178 } else {
5179 TypeExpr::Named(type_name)
5180 }
5181 } else {
5182 TypeExpr::Named(type_name)
5183 }
5184 } else {
5185 return None;
5186 };
5187
5188 if nullable {
5189 schema_type = match schema_type {
5190 TypeExpr::Union(mut members) => {
5191 if !members
5192 .iter()
5193 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil"))
5194 {
5195 members.push(TypeExpr::Named("nil".into()));
5196 }
5197 TypeExpr::Union(members)
5198 }
5199 other => TypeExpr::Union(vec![other, TypeExpr::Named("nil".into())]),
5200 };
5201 }
5202
5203 Some(schema_type)
5204}
5205
5206fn schema_union_type_expr(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
5207 let Node::ListLiteral(items) = &node.node else {
5208 return None;
5209 };
5210 let members = items
5211 .iter()
5212 .filter_map(|item| schema_type_expr_from_node(item, scope))
5213 .collect::<Vec<_>>();
5214 match members.len() {
5215 0 => None,
5216 1 => members.into_iter().next(),
5217 _ => Some(TypeExpr::Union(members)),
5218 }
5219}
5220
5221fn schema_required_names(node: &SNode) -> Option<Vec<String>> {
5222 let Node::ListLiteral(items) = &node.node else {
5223 return None;
5224 };
5225 Some(
5226 items
5227 .iter()
5228 .filter_map(|item| match &item.node {
5229 Node::StringLiteral(text) | Node::RawStringLiteral(text) => Some(text.clone()),
5230 Node::Identifier(text) => Some(text.clone()),
5231 _ => None,
5232 })
5233 .collect(),
5234 )
5235}
5236
5237fn schema_entry_key(node: &SNode) -> Option<String> {
5238 match &node.node {
5239 Node::Identifier(name) => Some(name.clone()),
5240 Node::StringLiteral(name) | Node::RawStringLiteral(name) => Some(name.clone()),
5241 _ => None,
5242 }
5243}
5244
5245fn normalize_schema_type_name(text: &str) -> String {
5246 match text {
5247 "object" => "dict".into(),
5248 "array" => "list".into(),
5249 "integer" => "int".into(),
5250 "number" => "float".into(),
5251 "boolean" => "bool".into(),
5252 "null" => "nil".into(),
5253 other => other.into(),
5254 }
5255}
5256
5257fn intersect_types(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
5258 match (current, schema_type) {
5259 (TypeExpr::LitString(a), TypeExpr::LitString(b)) if a == b => {
5261 Some(TypeExpr::LitString(a.clone()))
5262 }
5263 (TypeExpr::LitInt(a), TypeExpr::LitInt(b)) if a == b => Some(TypeExpr::LitInt(*a)),
5264 (TypeExpr::LitString(s), TypeExpr::Named(n))
5266 | (TypeExpr::Named(n), TypeExpr::LitString(s))
5267 if n == "string" =>
5268 {
5269 Some(TypeExpr::LitString(s.clone()))
5270 }
5271 (TypeExpr::LitInt(v), TypeExpr::Named(n)) | (TypeExpr::Named(n), TypeExpr::LitInt(v))
5272 if n == "int" || n == "float" =>
5273 {
5274 Some(TypeExpr::LitInt(*v))
5275 }
5276 (TypeExpr::Union(members), other) => {
5277 let kept = members
5278 .iter()
5279 .filter_map(|member| intersect_types(member, other))
5280 .collect::<Vec<_>>();
5281 match kept.len() {
5282 0 => None,
5283 1 => kept.into_iter().next(),
5284 _ => Some(TypeExpr::Union(kept)),
5285 }
5286 }
5287 (other, TypeExpr::Union(members)) => {
5288 let kept = members
5289 .iter()
5290 .filter_map(|member| intersect_types(other, member))
5291 .collect::<Vec<_>>();
5292 match kept.len() {
5293 0 => None,
5294 1 => kept.into_iter().next(),
5295 _ => Some(TypeExpr::Union(kept)),
5296 }
5297 }
5298 (TypeExpr::Named(left), TypeExpr::Named(right)) if left == right => {
5299 Some(TypeExpr::Named(left.clone()))
5300 }
5301 (TypeExpr::Named(name), TypeExpr::Shape(fields)) if name == "dict" => {
5302 Some(TypeExpr::Shape(fields.clone()))
5303 }
5304 (TypeExpr::Shape(fields), TypeExpr::Named(name)) if name == "dict" => {
5305 Some(TypeExpr::Shape(fields.clone()))
5306 }
5307 (TypeExpr::Named(name), TypeExpr::List(inner)) if name == "list" => {
5308 Some(TypeExpr::List(inner.clone()))
5309 }
5310 (TypeExpr::List(inner), TypeExpr::Named(name)) if name == "list" => {
5311 Some(TypeExpr::List(inner.clone()))
5312 }
5313 (TypeExpr::Named(name), TypeExpr::DictType(key, value)) if name == "dict" => {
5314 Some(TypeExpr::DictType(key.clone(), value.clone()))
5315 }
5316 (TypeExpr::DictType(key, value), TypeExpr::Named(name)) if name == "dict" => {
5317 Some(TypeExpr::DictType(key.clone(), value.clone()))
5318 }
5319 (TypeExpr::Shape(_), TypeExpr::Shape(fields)) => Some(TypeExpr::Shape(fields.clone())),
5320 (TypeExpr::List(current_inner), TypeExpr::List(schema_inner)) => {
5321 intersect_types(current_inner, schema_inner)
5322 .map(|inner| TypeExpr::List(Box::new(inner)))
5323 }
5324 (
5325 TypeExpr::DictType(current_key, current_value),
5326 TypeExpr::DictType(schema_key, schema_value),
5327 ) => {
5328 let key = intersect_types(current_key, schema_key)?;
5329 let value = intersect_types(current_value, schema_value)?;
5330 Some(TypeExpr::DictType(Box::new(key), Box::new(value)))
5331 }
5332 _ => None,
5333 }
5334}
5335
5336fn subtract_type(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
5337 match current {
5338 TypeExpr::Union(members) => {
5339 let remaining = members
5340 .iter()
5341 .filter(|member| intersect_types(member, schema_type).is_none())
5342 .cloned()
5343 .collect::<Vec<_>>();
5344 match remaining.len() {
5345 0 => None,
5346 1 => remaining.into_iter().next(),
5347 _ => Some(TypeExpr::Union(remaining)),
5348 }
5349 }
5350 other if intersect_types(other, schema_type).is_some() => None,
5351 other => Some(other.clone()),
5352 }
5353}
5354
5355fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
5357 for (var_name, narrowed_type) in refinements {
5358 if !scope.narrowed_vars.contains_key(var_name) {
5360 if let Some(original) = scope.get_var(var_name).cloned() {
5361 scope.narrowed_vars.insert(var_name.clone(), original);
5362 }
5363 }
5364 scope.define_var(var_name, narrowed_type.clone());
5365 }
5366}
5367
5368#[cfg(test)]
5369mod tests {
5370 use super::*;
5371 use crate::Parser;
5372 use harn_lexer::Lexer;
5373
5374 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
5375 let mut lexer = Lexer::new(source);
5376 let tokens = lexer.tokenize().unwrap();
5377 let mut parser = Parser::new(tokens);
5378 let program = parser.parse().unwrap();
5379 TypeChecker::new().check(&program)
5380 }
5381
5382 fn errors(source: &str) -> Vec<String> {
5383 check_source(source)
5384 .into_iter()
5385 .filter(|d| d.severity == DiagnosticSeverity::Error)
5386 .map(|d| d.message)
5387 .collect()
5388 }
5389
5390 #[test]
5391 fn test_no_errors_for_untyped_code() {
5392 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
5393 assert!(errs.is_empty());
5394 }
5395
5396 #[test]
5397 fn test_correct_typed_let() {
5398 let errs = errors("pipeline t(task) { let x: int = 42 }");
5399 assert!(errs.is_empty());
5400 }
5401
5402 #[test]
5403 fn test_type_mismatch_let() {
5404 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
5405 assert_eq!(errs.len(), 1);
5406 assert!(errs[0].contains("declared as int"));
5407 assert!(errs[0].contains("assigned string"));
5408 }
5409
5410 #[test]
5411 fn test_correct_typed_fn() {
5412 let errs = errors(
5413 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
5414 );
5415 assert!(errs.is_empty());
5416 }
5417
5418 #[test]
5419 fn test_fn_arg_type_mismatch() {
5420 let errs = errors(
5421 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
5422add("hello", 2) }"#,
5423 );
5424 assert_eq!(errs.len(), 1);
5425 assert!(errs[0].contains("Argument 1"));
5426 assert!(errs[0].contains("expected int"));
5427 }
5428
5429 #[test]
5430 fn test_return_type_mismatch() {
5431 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
5432 assert_eq!(errs.len(), 1);
5433 assert!(errs[0].contains("return type doesn't match"));
5434 }
5435
5436 #[test]
5437 fn test_union_type_compatible() {
5438 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
5439 assert!(errs.is_empty());
5440 }
5441
5442 #[test]
5443 fn test_union_type_mismatch() {
5444 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
5445 assert_eq!(errs.len(), 1);
5446 assert!(errs[0].contains("declared as"));
5447 }
5448
5449 #[test]
5450 fn test_type_inference_propagation() {
5451 let errs = errors(
5452 r#"pipeline t(task) {
5453 fn add(a: int, b: int) -> int { return a + b }
5454 let result: string = add(1, 2)
5455}"#,
5456 );
5457 assert_eq!(errs.len(), 1);
5458 assert!(errs[0].contains("declared as"));
5459 assert!(errs[0].contains("string"));
5460 assert!(errs[0].contains("int"));
5461 }
5462
5463 #[test]
5464 fn test_generic_return_type_instantiates_from_callsite() {
5465 let errs = errors(
5466 r#"pipeline t(task) {
5467 fn identity<T>(x: T) -> T { return x }
5468 fn first<T>(items: list<T>) -> T { return items[0] }
5469 let n: int = identity(42)
5470 let s: string = first(["a", "b"])
5471}"#,
5472 );
5473 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
5474 }
5475
5476 #[test]
5477 fn test_generic_type_param_must_bind_consistently() {
5478 let errs = errors(
5479 r#"pipeline t(task) {
5480 fn keep<T>(a: T, b: T) -> T { return a }
5481 keep(1, "x")
5482}"#,
5483 );
5484 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
5485 assert!(
5486 errs.iter()
5487 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
5488 "missing generic binding conflict error: {:?}",
5489 errs
5490 );
5491 assert!(
5492 errs.iter()
5493 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
5494 "missing instantiated argument mismatch error: {:?}",
5495 errs
5496 );
5497 }
5498
5499 #[test]
5500 fn test_generic_list_binding_propagates_element_type() {
5501 let errs = errors(
5502 r#"pipeline t(task) {
5503 fn first<T>(items: list<T>) -> T { return items[0] }
5504 let bad: string = first([1, 2, 3])
5505}"#,
5506 );
5507 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
5508 assert!(errs[0].contains("declared as string, but assigned int"));
5509 }
5510
5511 #[test]
5512 fn test_generic_struct_literal_instantiates_type_arguments() {
5513 let errs = errors(
5514 r#"pipeline t(task) {
5515 struct Pair<A, B> {
5516 first: A
5517 second: B
5518 }
5519 let pair: Pair<int, string> = Pair { first: 1, second: "two" }
5520}"#,
5521 );
5522 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
5523 }
5524
5525 #[test]
5526 fn test_generic_enum_construct_instantiates_type_arguments() {
5527 let errs = errors(
5528 r#"pipeline t(task) {
5529 enum Option<T> {
5530 Some(value: T),
5531 None
5532 }
5533 let value: Option<int> = Option.Some(42)
5534}"#,
5535 );
5536 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
5537 }
5538
5539 #[test]
5540 fn test_result_generic_type_compatibility() {
5541 let errs = errors(
5542 r#"pipeline t(task) {
5543 let ok: Result<int, string> = Result.Ok(42)
5544 let err: Result<int, string> = Result.Err("oops")
5545}"#,
5546 );
5547 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
5548 }
5549
5550 #[test]
5551 fn test_result_generic_type_mismatch_reports_error() {
5552 let errs = errors(
5553 r#"pipeline t(task) {
5554 let bad: Result<int, string> = Result.Err(42)
5555}"#,
5556 );
5557 assert_eq!(errs.len(), 1, "expected 1 error, got: {errs:?}");
5558 assert!(errs[0].contains("Result<int, string>"));
5559 assert!(errs[0].contains("Result<_, int>"));
5560 }
5561
5562 #[test]
5563 fn test_builtin_return_type_inference() {
5564 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
5565 assert_eq!(errs.len(), 1);
5566 assert!(errs[0].contains("string"));
5567 assert!(errs[0].contains("int"));
5568 }
5569
5570 #[test]
5571 fn test_workflow_and_transcript_builtins_are_known() {
5572 let errs = errors(
5573 r#"pipeline t(task) {
5574 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
5575 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
5576 let run: dict = workflow_execute("task", flow, [], {})
5577 let tree: dict = load_run_tree("run.json")
5578 let fixture: dict = run_record_fixture(run?.run)
5579 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
5580 let diff: dict = run_record_diff(run?.run, run?.run)
5581 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
5582 let suite_report: dict = eval_suite_run(manifest)
5583 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
5584 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
5585 let selection: dict = artifact_editor_selection("src/main.rs", "main")
5586 let verify: dict = artifact_verification_result("verify", "ok")
5587 let test_result: dict = artifact_test_result("tests", "pass")
5588 let cmd: dict = artifact_command_result("cargo test", {status: 0})
5589 let patch: dict = artifact_diff("src/main.rs", "old", "new")
5590 let git: dict = artifact_git_diff("diff --git a b")
5591 let review: dict = artifact_diff_review(patch, "review me")
5592 let decision: dict = artifact_review_decision(review, "accepted")
5593 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
5594 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
5595 let apply: dict = artifact_apply_intent(review, "apply")
5596 let transcript = transcript_reset({metadata: {source: "test"}})
5597 let visible: string = transcript_render_visible(transcript_archive(transcript))
5598 let events: list = transcript_events(transcript)
5599 let context: string = artifact_context([], {max_artifacts: 1})
5600 println(report)
5601 println(run)
5602 println(tree)
5603 println(fixture)
5604 println(suite)
5605 println(diff)
5606 println(manifest)
5607 println(suite_report)
5608 println(wf)
5609 println(snap)
5610 println(selection)
5611 println(verify)
5612 println(test_result)
5613 println(cmd)
5614 println(patch)
5615 println(git)
5616 println(review)
5617 println(decision)
5618 println(proposal)
5619 println(bundle)
5620 println(apply)
5621 println(visible)
5622 println(events)
5623 println(context)
5624}"#,
5625 );
5626 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
5627 }
5628
5629 #[test]
5630 fn test_binary_op_type_inference() {
5631 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
5632 assert_eq!(errs.len(), 1);
5633 }
5634
5635 #[test]
5636 fn test_exponentiation_requires_numeric_operands() {
5637 let errs = errors(r#"pipeline t(task) { let x = "nope" ** 2 }"#);
5638 assert!(
5639 errs.iter().any(|err| err.contains("can't use '**'")),
5640 "missing exponentiation type error: {errs:?}"
5641 );
5642 }
5643
5644 #[test]
5645 fn test_comparison_returns_bool() {
5646 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
5647 assert!(errs.is_empty());
5648 }
5649
5650 #[test]
5651 fn test_int_float_promotion() {
5652 let errs = errors("pipeline t(task) { let x: float = 42 }");
5653 assert!(errs.is_empty());
5654 }
5655
5656 #[test]
5657 fn test_untyped_code_no_errors() {
5658 let errs = errors(
5659 r#"pipeline t(task) {
5660 fn process(data) {
5661 let result = data + " processed"
5662 return result
5663 }
5664 log(process("hello"))
5665}"#,
5666 );
5667 assert!(errs.is_empty());
5668 }
5669
5670 #[test]
5671 fn test_type_alias() {
5672 let errs = errors(
5673 r#"pipeline t(task) {
5674 type Name = string
5675 let x: Name = "hello"
5676}"#,
5677 );
5678 assert!(errs.is_empty());
5679 }
5680
5681 #[test]
5682 fn test_type_alias_mismatch() {
5683 let errs = errors(
5684 r#"pipeline t(task) {
5685 type Name = string
5686 let x: Name = 42
5687}"#,
5688 );
5689 assert_eq!(errs.len(), 1);
5690 }
5691
5692 #[test]
5693 fn test_assignment_type_check() {
5694 let errs = errors(
5695 r#"pipeline t(task) {
5696 var x: int = 0
5697 x = "hello"
5698}"#,
5699 );
5700 assert_eq!(errs.len(), 1);
5701 assert!(errs[0].contains("can't assign string"));
5702 }
5703
5704 #[test]
5705 fn test_covariance_int_to_float_in_fn() {
5706 let errs = errors(
5707 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
5708 );
5709 assert!(errs.is_empty());
5710 }
5711
5712 #[test]
5713 fn test_covariance_return_type() {
5714 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
5715 assert!(errs.is_empty());
5716 }
5717
5718 #[test]
5719 fn test_no_contravariance_float_to_int() {
5720 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
5721 assert_eq!(errs.len(), 1);
5722 }
5723
5724 #[test]
5727 fn test_fn_param_contravariance_positive() {
5728 let errs = errors(
5732 r#"pipeline t(task) {
5733 let wide = fn(x: float) { return 0 }
5734 let cb: fn(int) -> int = wide
5735 }"#,
5736 );
5737 assert!(
5738 errs.is_empty(),
5739 "expected fn(float)->int to satisfy fn(int)->int, got: {errs:?}"
5740 );
5741 }
5742
5743 #[test]
5744 fn test_fn_param_contravariance_negative() {
5745 let errs = errors(
5749 r#"pipeline t(task) {
5750 let narrow = fn(x: int) { return 0 }
5751 let cb: fn(float) -> int = narrow
5752 }"#,
5753 );
5754 assert!(
5755 !errs.is_empty(),
5756 "expected fn(int)->int NOT to satisfy fn(float)->int, but type-check passed"
5757 );
5758 }
5759
5760 #[test]
5761 fn test_list_invariant_int_to_float_rejected() {
5762 let errs = errors(
5765 r#"pipeline t(task) {
5766 let xs: list<int> = [1, 2, 3]
5767 let ys: list<float> = xs
5768 }"#,
5769 );
5770 assert!(
5771 !errs.is_empty(),
5772 "expected list<int> NOT to flow into list<float>, but type-check passed"
5773 );
5774 }
5775
5776 #[test]
5777 fn test_iter_covariant_int_to_float_accepted() {
5778 let errs = errors(
5780 r#"pipeline t(task) {
5781 fn sink(ys: iter<float>) -> int { return 0 }
5782 fn pipe(xs: iter<int>) -> int { return sink(xs) }
5783 }"#,
5784 );
5785 assert!(
5786 errs.is_empty(),
5787 "expected iter<int> to flow into iter<float>, got: {errs:?}"
5788 );
5789 }
5790
5791 #[test]
5792 fn test_decl_site_out_used_in_contravariant_position_rejected() {
5793 let errs = errors(
5797 r#"pipeline t(task) {
5798 type Box<out T> = fn(T) -> int
5799 }"#,
5800 );
5801 assert!(
5802 errs.iter().any(|e| e.contains("declared 'out'")),
5803 "expected 'out T' misuse diagnostic, got: {errs:?}"
5804 );
5805 }
5806
5807 #[test]
5808 fn test_decl_site_in_used_in_covariant_position_rejected() {
5809 let errs = errors(
5812 r#"pipeline t(task) {
5813 interface Producer<in T> { fn next() -> T }
5814 }"#,
5815 );
5816 assert!(
5817 errs.iter().any(|e| e.contains("declared 'in'")),
5818 "expected 'in T' misuse diagnostic, got: {errs:?}"
5819 );
5820 }
5821
5822 #[test]
5823 fn test_decl_site_out_in_covariant_position_ok() {
5824 let errs = errors(
5827 r#"pipeline t(task) {
5828 type Reader<out T> = fn() -> T
5829 }"#,
5830 );
5831 assert!(
5832 errs.iter().all(|e| !e.contains("declared 'out'")),
5833 "unexpected variance diagnostic: {errs:?}"
5834 );
5835 }
5836
5837 #[test]
5838 fn test_dict_invariant_int_to_float_rejected() {
5839 let errs = errors(
5840 r#"pipeline t(task) {
5841 let d: dict<string, int> = {"a": 1}
5842 let e: dict<string, float> = d
5843 }"#,
5844 );
5845 assert!(
5846 !errs.is_empty(),
5847 "expected dict<string, int> NOT to flow into dict<string, float>"
5848 );
5849 }
5850
5851 fn warnings(source: &str) -> Vec<String> {
5852 check_source(source)
5853 .into_iter()
5854 .filter(|d| d.severity == DiagnosticSeverity::Warning)
5855 .map(|d| d.message)
5856 .collect()
5857 }
5858
5859 #[test]
5860 fn test_exhaustive_match_no_warning() {
5861 let warns = warnings(
5862 r#"pipeline t(task) {
5863 enum Color { Red, Green, Blue }
5864 let c = Color.Red
5865 match c.variant {
5866 "Red" -> { log("r") }
5867 "Green" -> { log("g") }
5868 "Blue" -> { log("b") }
5869 }
5870}"#,
5871 );
5872 let exhaustive_warns: Vec<_> = warns
5873 .iter()
5874 .filter(|w| w.contains("Non-exhaustive"))
5875 .collect();
5876 assert!(exhaustive_warns.is_empty());
5877 }
5878
5879 #[test]
5880 fn test_non_exhaustive_match_warning() {
5881 let warns = warnings(
5882 r#"pipeline t(task) {
5883 enum Color { Red, Green, Blue }
5884 let c = Color.Red
5885 match c.variant {
5886 "Red" -> { log("r") }
5887 "Green" -> { log("g") }
5888 }
5889}"#,
5890 );
5891 let exhaustive_warns: Vec<_> = warns
5892 .iter()
5893 .filter(|w| w.contains("Non-exhaustive"))
5894 .collect();
5895 assert_eq!(exhaustive_warns.len(), 1);
5896 assert!(exhaustive_warns[0].contains("Blue"));
5897 }
5898
5899 #[test]
5900 fn test_non_exhaustive_multiple_missing() {
5901 let warns = warnings(
5902 r#"pipeline t(task) {
5903 enum Status { Active, Inactive, Pending }
5904 let s = Status.Active
5905 match s.variant {
5906 "Active" -> { log("a") }
5907 }
5908}"#,
5909 );
5910 let exhaustive_warns: Vec<_> = warns
5911 .iter()
5912 .filter(|w| w.contains("Non-exhaustive"))
5913 .collect();
5914 assert_eq!(exhaustive_warns.len(), 1);
5915 assert!(exhaustive_warns[0].contains("Inactive"));
5916 assert!(exhaustive_warns[0].contains("Pending"));
5917 }
5918
5919 fn exhaustive_warns(source: &str) -> Vec<String> {
5920 warnings(source)
5921 .into_iter()
5922 .filter(|w| w.contains("was not fully narrowed"))
5923 .collect()
5924 }
5925
5926 #[test]
5927 fn test_unknown_exhaustive_unreachable_happy_path() {
5928 let source = r#"pipeline t(task) {
5930 fn describe(v: unknown) -> string {
5931 if type_of(v) == "string" { return "s" }
5932 if type_of(v) == "int" { return "i" }
5933 if type_of(v) == "float" { return "f" }
5934 if type_of(v) == "bool" { return "b" }
5935 if type_of(v) == "nil" { return "n" }
5936 if type_of(v) == "list" { return "l" }
5937 if type_of(v) == "dict" { return "d" }
5938 if type_of(v) == "closure" { return "c" }
5939 unreachable("unknown type_of variant")
5940 }
5941 log(describe(1))
5942}"#;
5943 assert!(exhaustive_warns(source).is_empty());
5944 }
5945
5946 #[test]
5947 fn test_unknown_exhaustive_unreachable_incomplete_warns() {
5948 let source = r#"pipeline t(task) {
5949 fn describe(v: unknown) -> string {
5950 if type_of(v) == "string" { return "s" }
5951 if type_of(v) == "int" { return "i" }
5952 unreachable("unknown type_of variant")
5953 }
5954 log(describe(1))
5955}"#;
5956 let warns = exhaustive_warns(source);
5957 assert_eq!(warns.len(), 1, "expected one warning, got: {:?}", warns);
5958 let w = &warns[0];
5959 for missing in &["float", "bool", "nil", "list", "dict", "closure"] {
5960 assert!(w.contains(missing), "missing {missing} in: {w}");
5961 }
5962 assert!(!w.contains("int"));
5963 assert!(!w.contains("string"));
5964 assert!(w.contains("unreachable"));
5965 assert!(w.contains("v: unknown"));
5966 }
5967
5968 #[test]
5969 fn test_unknown_incomplete_normal_return_no_warning() {
5970 let source = r#"pipeline t(task) {
5972 fn describe(v: unknown) -> string {
5973 if type_of(v) == "string" { return "s" }
5974 if type_of(v) == "int" { return "i" }
5975 return "other"
5976 }
5977 log(describe(1))
5978}"#;
5979 assert!(exhaustive_warns(source).is_empty());
5980 }
5981
5982 #[test]
5983 fn test_unknown_exhaustive_throw_incomplete_warns() {
5984 let source = r#"pipeline t(task) {
5985 fn describe(v: unknown) -> string {
5986 if type_of(v) == "string" { return "s" }
5987 throw "unhandled"
5988 }
5989 log(describe("x"))
5990}"#;
5991 let warns = exhaustive_warns(source);
5992 assert_eq!(warns.len(), 1, "expected one warning, got: {:?}", warns);
5993 assert!(warns[0].contains("throw"));
5994 assert!(warns[0].contains("int"));
5995 }
5996
5997 #[test]
5998 fn test_unknown_throw_without_narrowing_no_warning() {
5999 let source = r#"pipeline t(task) {
6002 fn crash(v: unknown) -> string {
6003 throw "nope"
6004 }
6005 log(crash(1))
6006}"#;
6007 assert!(exhaustive_warns(source).is_empty());
6008 }
6009
6010 #[test]
6011 fn test_unknown_exhaustive_nested_branch() {
6012 let source = r#"pipeline t(task) {
6015 fn describe(v: unknown, x: int) -> string {
6016 if type_of(v) == "string" {
6017 if x > 0 { return v.upper() } else { return "s" }
6018 }
6019 if type_of(v) == "int" { return "i" }
6020 unreachable("unknown type_of variant")
6021 }
6022 log(describe(1, 1))
6023}"#;
6024 let warns = exhaustive_warns(source);
6025 assert_eq!(warns.len(), 1, "expected one warning, got: {:?}", warns);
6026 assert!(warns[0].contains("float"));
6027 }
6028
6029 #[test]
6030 fn test_unknown_exhaustive_negated_check() {
6031 let source = r#"pipeline t(task) {
6034 fn describe(v: unknown) -> string {
6035 if type_of(v) != "string" {
6036 // v still unknown here, but "string" is NOT ruled out on this path
6037 return "non-string"
6038 }
6039 // v: string here
6040 return v.upper()
6041 }
6042 log(describe("x"))
6043}"#;
6044 assert!(exhaustive_warns(source).is_empty());
6046 }
6047
6048 #[test]
6049 fn test_enum_construct_type_inference() {
6050 let errs = errors(
6051 r#"pipeline t(task) {
6052 enum Color { Red, Green, Blue }
6053 let c: Color = Color.Red
6054}"#,
6055 );
6056 assert!(errs.is_empty());
6057 }
6058
6059 #[test]
6060 fn test_nil_coalescing_strips_nil() {
6061 let errs = errors(
6063 r#"pipeline t(task) {
6064 let x: string | nil = nil
6065 let y: string = x ?? "default"
6066}"#,
6067 );
6068 assert!(errs.is_empty());
6069 }
6070
6071 #[test]
6072 fn test_shape_mismatch_detail_missing_field() {
6073 let errs = errors(
6074 r#"pipeline t(task) {
6075 let x: {name: string, age: int} = {name: "hello"}
6076}"#,
6077 );
6078 assert_eq!(errs.len(), 1);
6079 assert!(
6080 errs[0].contains("missing field 'age'"),
6081 "expected detail about missing field, got: {}",
6082 errs[0]
6083 );
6084 }
6085
6086 #[test]
6087 fn test_shape_mismatch_detail_wrong_type() {
6088 let errs = errors(
6089 r#"pipeline t(task) {
6090 let x: {name: string, age: int} = {name: 42, age: 10}
6091}"#,
6092 );
6093 assert_eq!(errs.len(), 1);
6094 assert!(
6095 errs[0].contains("field 'name' has type int, expected string"),
6096 "expected detail about wrong type, got: {}",
6097 errs[0]
6098 );
6099 }
6100
6101 #[test]
6102 fn test_match_pattern_string_against_int() {
6103 let warns = warnings(
6104 r#"pipeline t(task) {
6105 let x: int = 42
6106 match x {
6107 "hello" -> { log("bad") }
6108 42 -> { log("ok") }
6109 }
6110}"#,
6111 );
6112 let pattern_warns: Vec<_> = warns
6113 .iter()
6114 .filter(|w| w.contains("Match pattern type mismatch"))
6115 .collect();
6116 assert_eq!(pattern_warns.len(), 1);
6117 assert!(pattern_warns[0].contains("matching int against string literal"));
6118 }
6119
6120 #[test]
6121 fn test_match_pattern_int_against_string() {
6122 let warns = warnings(
6123 r#"pipeline t(task) {
6124 let x: string = "hello"
6125 match x {
6126 42 -> { log("bad") }
6127 "hello" -> { log("ok") }
6128 }
6129}"#,
6130 );
6131 let pattern_warns: Vec<_> = warns
6132 .iter()
6133 .filter(|w| w.contains("Match pattern type mismatch"))
6134 .collect();
6135 assert_eq!(pattern_warns.len(), 1);
6136 assert!(pattern_warns[0].contains("matching string against int literal"));
6137 }
6138
6139 #[test]
6140 fn test_match_pattern_bool_against_int() {
6141 let warns = warnings(
6142 r#"pipeline t(task) {
6143 let x: int = 42
6144 match x {
6145 true -> { log("bad") }
6146 42 -> { log("ok") }
6147 }
6148}"#,
6149 );
6150 let pattern_warns: Vec<_> = warns
6151 .iter()
6152 .filter(|w| w.contains("Match pattern type mismatch"))
6153 .collect();
6154 assert_eq!(pattern_warns.len(), 1);
6155 assert!(pattern_warns[0].contains("matching int against bool literal"));
6156 }
6157
6158 #[test]
6159 fn test_match_pattern_float_against_string() {
6160 let warns = warnings(
6161 r#"pipeline t(task) {
6162 let x: string = "hello"
6163 match x {
6164 3.14 -> { log("bad") }
6165 "hello" -> { log("ok") }
6166 }
6167}"#,
6168 );
6169 let pattern_warns: Vec<_> = warns
6170 .iter()
6171 .filter(|w| w.contains("Match pattern type mismatch"))
6172 .collect();
6173 assert_eq!(pattern_warns.len(), 1);
6174 assert!(pattern_warns[0].contains("matching string against float literal"));
6175 }
6176
6177 #[test]
6178 fn test_match_pattern_int_against_float_ok() {
6179 let warns = warnings(
6181 r#"pipeline t(task) {
6182 let x: float = 3.14
6183 match x {
6184 42 -> { log("ok") }
6185 _ -> { log("default") }
6186 }
6187}"#,
6188 );
6189 let pattern_warns: Vec<_> = warns
6190 .iter()
6191 .filter(|w| w.contains("Match pattern type mismatch"))
6192 .collect();
6193 assert!(pattern_warns.is_empty());
6194 }
6195
6196 #[test]
6197 fn test_match_pattern_float_against_int_ok() {
6198 let warns = warnings(
6200 r#"pipeline t(task) {
6201 let x: int = 42
6202 match x {
6203 3.14 -> { log("close") }
6204 _ -> { log("default") }
6205 }
6206}"#,
6207 );
6208 let pattern_warns: Vec<_> = warns
6209 .iter()
6210 .filter(|w| w.contains("Match pattern type mismatch"))
6211 .collect();
6212 assert!(pattern_warns.is_empty());
6213 }
6214
6215 #[test]
6216 fn test_match_pattern_correct_types_no_warning() {
6217 let warns = warnings(
6218 r#"pipeline t(task) {
6219 let x: int = 42
6220 match x {
6221 1 -> { log("one") }
6222 2 -> { log("two") }
6223 _ -> { log("other") }
6224 }
6225}"#,
6226 );
6227 let pattern_warns: Vec<_> = warns
6228 .iter()
6229 .filter(|w| w.contains("Match pattern type mismatch"))
6230 .collect();
6231 assert!(pattern_warns.is_empty());
6232 }
6233
6234 #[test]
6235 fn test_match_pattern_wildcard_no_warning() {
6236 let warns = warnings(
6237 r#"pipeline t(task) {
6238 let x: int = 42
6239 match x {
6240 _ -> { log("catch all") }
6241 }
6242}"#,
6243 );
6244 let pattern_warns: Vec<_> = warns
6245 .iter()
6246 .filter(|w| w.contains("Match pattern type mismatch"))
6247 .collect();
6248 assert!(pattern_warns.is_empty());
6249 }
6250
6251 #[test]
6252 fn test_match_pattern_untyped_no_warning() {
6253 let warns = warnings(
6255 r#"pipeline t(task) {
6256 let x = some_unknown_fn()
6257 match x {
6258 "hello" -> { log("string") }
6259 42 -> { log("int") }
6260 }
6261}"#,
6262 );
6263 let pattern_warns: Vec<_> = warns
6264 .iter()
6265 .filter(|w| w.contains("Match pattern type mismatch"))
6266 .collect();
6267 assert!(pattern_warns.is_empty());
6268 }
6269
6270 fn iface_errors(source: &str) -> Vec<String> {
6271 errors(source)
6272 .into_iter()
6273 .filter(|message| message.contains("does not satisfy interface"))
6274 .collect()
6275 }
6276
6277 #[test]
6278 fn test_interface_constraint_return_type_mismatch() {
6279 let warns = iface_errors(
6280 r#"pipeline t(task) {
6281 interface Sizable {
6282 fn size(self) -> int
6283 }
6284 struct Box { width: int }
6285 impl Box {
6286 fn size(self) -> string { return "nope" }
6287 }
6288 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
6289 measure(Box({width: 3}))
6290}"#,
6291 );
6292 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
6293 assert!(
6294 warns[0].contains("method 'size' returns 'string', expected 'int'"),
6295 "unexpected message: {}",
6296 warns[0]
6297 );
6298 }
6299
6300 #[test]
6301 fn test_interface_constraint_param_type_mismatch() {
6302 let warns = iface_errors(
6303 r#"pipeline t(task) {
6304 interface Processor {
6305 fn process(self, x: int) -> string
6306 }
6307 struct MyProc { name: string }
6308 impl MyProc {
6309 fn process(self, x: string) -> string { return x }
6310 }
6311 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
6312 run_proc(MyProc({name: "a"}))
6313}"#,
6314 );
6315 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
6316 assert!(
6317 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
6318 "unexpected message: {}",
6319 warns[0]
6320 );
6321 }
6322
6323 #[test]
6324 fn test_interface_constraint_missing_method() {
6325 let warns = iface_errors(
6326 r#"pipeline t(task) {
6327 interface Sizable {
6328 fn size(self) -> int
6329 }
6330 struct Box { width: int }
6331 impl Box {
6332 fn area(self) -> int { return self.width }
6333 }
6334 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
6335 measure(Box({width: 3}))
6336}"#,
6337 );
6338 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
6339 assert!(
6340 warns[0].contains("missing method 'size'"),
6341 "unexpected message: {}",
6342 warns[0]
6343 );
6344 }
6345
6346 #[test]
6347 fn test_interface_constraint_param_count_mismatch() {
6348 let warns = iface_errors(
6349 r#"pipeline t(task) {
6350 interface Doubler {
6351 fn double(self, x: int) -> int
6352 }
6353 struct Bad { v: int }
6354 impl Bad {
6355 fn double(self) -> int { return self.v * 2 }
6356 }
6357 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
6358 run_double(Bad({v: 5}))
6359}"#,
6360 );
6361 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
6362 assert!(
6363 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
6364 "unexpected message: {}",
6365 warns[0]
6366 );
6367 }
6368
6369 #[test]
6370 fn test_interface_constraint_satisfied() {
6371 let warns = iface_errors(
6372 r#"pipeline t(task) {
6373 interface Sizable {
6374 fn size(self) -> int
6375 }
6376 struct Box { width: int, height: int }
6377 impl Box {
6378 fn size(self) -> int { return self.width * self.height }
6379 }
6380 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
6381 measure(Box({width: 3, height: 4}))
6382}"#,
6383 );
6384 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
6385 }
6386
6387 #[test]
6388 fn test_interface_constraint_untyped_impl_compatible() {
6389 let warns = iface_errors(
6391 r#"pipeline t(task) {
6392 interface Sizable {
6393 fn size(self) -> int
6394 }
6395 struct Box { width: int }
6396 impl Box {
6397 fn size(self) { return self.width }
6398 }
6399 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
6400 measure(Box({width: 3}))
6401}"#,
6402 );
6403 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
6404 }
6405
6406 #[test]
6407 fn test_interface_constraint_int_float_covariance() {
6408 let warns = iface_errors(
6410 r#"pipeline t(task) {
6411 interface Measurable {
6412 fn value(self) -> float
6413 }
6414 struct Gauge { v: int }
6415 impl Gauge {
6416 fn value(self) -> int { return self.v }
6417 }
6418 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
6419 read_val(Gauge({v: 42}))
6420}"#,
6421 );
6422 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
6423 }
6424
6425 #[test]
6426 fn test_interface_associated_type_constraint_satisfied() {
6427 let warns = iface_errors(
6428 r#"pipeline t(task) {
6429 interface Collection {
6430 type Item
6431 fn get(self, index: int) -> Item
6432 }
6433 struct Names {}
6434 impl Names {
6435 fn get(self, index: int) -> string { return "ada" }
6436 }
6437 fn first<C>(collection: C) where C: Collection {
6438 log(collection.get(0))
6439 }
6440 first(Names {})
6441}"#,
6442 );
6443 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
6444 }
6445
6446 #[test]
6447 fn test_interface_associated_type_default_mismatch() {
6448 let warns = iface_errors(
6449 r#"pipeline t(task) {
6450 interface IntCollection {
6451 type Item = int
6452 fn get(self, index: int) -> Item
6453 }
6454 struct Labels {}
6455 impl Labels {
6456 fn get(self, index: int) -> string { return "oops" }
6457 }
6458 fn first<C>(collection: C) where C: IntCollection {
6459 log(collection.get(0))
6460 }
6461 first(Labels {})
6462}"#,
6463 );
6464 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
6465 assert!(
6466 warns[0].contains("associated type 'Item' resolves to 'string', expected 'int'"),
6467 "unexpected message: {}",
6468 warns[0]
6469 );
6470 }
6471
6472 #[test]
6473 fn test_nil_narrowing_then_branch() {
6474 let errs = errors(
6476 r#"pipeline t(task) {
6477 fn greet(name: string | nil) {
6478 if name != nil {
6479 let s: string = name
6480 }
6481 }
6482}"#,
6483 );
6484 assert!(errs.is_empty(), "got: {:?}", errs);
6485 }
6486
6487 #[test]
6488 fn test_nil_narrowing_else_branch() {
6489 let errs = errors(
6491 r#"pipeline t(task) {
6492 fn check(x: string | nil) {
6493 if x != nil {
6494 let s: string = x
6495 } else {
6496 let n: nil = x
6497 }
6498 }
6499}"#,
6500 );
6501 assert!(errs.is_empty(), "got: {:?}", errs);
6502 }
6503
6504 #[test]
6505 fn test_nil_equality_narrows_both() {
6506 let errs = errors(
6508 r#"pipeline t(task) {
6509 fn check(x: string | nil) {
6510 if x == nil {
6511 let n: nil = x
6512 } else {
6513 let s: string = x
6514 }
6515 }
6516}"#,
6517 );
6518 assert!(errs.is_empty(), "got: {:?}", errs);
6519 }
6520
6521 #[test]
6522 fn test_truthiness_narrowing() {
6523 let errs = errors(
6525 r#"pipeline t(task) {
6526 fn check(x: string | nil) {
6527 if x {
6528 let s: string = x
6529 }
6530 }
6531}"#,
6532 );
6533 assert!(errs.is_empty(), "got: {:?}", errs);
6534 }
6535
6536 #[test]
6537 fn test_negation_narrowing() {
6538 let errs = errors(
6540 r#"pipeline t(task) {
6541 fn check(x: string | nil) {
6542 if !x {
6543 let n: nil = x
6544 } else {
6545 let s: string = x
6546 }
6547 }
6548}"#,
6549 );
6550 assert!(errs.is_empty(), "got: {:?}", errs);
6551 }
6552
6553 #[test]
6554 fn test_typeof_narrowing() {
6555 let errs = errors(
6557 r#"pipeline t(task) {
6558 fn check(x: string | int) {
6559 if type_of(x) == "string" {
6560 let s: string = x
6561 }
6562 }
6563}"#,
6564 );
6565 assert!(errs.is_empty(), "got: {:?}", errs);
6566 }
6567
6568 #[test]
6569 fn test_typeof_narrowing_else() {
6570 let errs = errors(
6572 r#"pipeline t(task) {
6573 fn check(x: string | int) {
6574 if type_of(x) == "string" {
6575 let s: string = x
6576 } else {
6577 let i: int = x
6578 }
6579 }
6580}"#,
6581 );
6582 assert!(errs.is_empty(), "got: {:?}", errs);
6583 }
6584
6585 #[test]
6586 fn test_typeof_neq_narrowing() {
6587 let errs = errors(
6589 r#"pipeline t(task) {
6590 fn check(x: string | int) {
6591 if type_of(x) != "string" {
6592 let i: int = x
6593 } else {
6594 let s: string = x
6595 }
6596 }
6597}"#,
6598 );
6599 assert!(errs.is_empty(), "got: {:?}", errs);
6600 }
6601
6602 #[test]
6603 fn test_and_combines_narrowing() {
6604 let errs = errors(
6606 r#"pipeline t(task) {
6607 fn check(x: string | int | nil) {
6608 if x != nil && type_of(x) == "string" {
6609 let s: string = x
6610 }
6611 }
6612}"#,
6613 );
6614 assert!(errs.is_empty(), "got: {:?}", errs);
6615 }
6616
6617 #[test]
6618 fn test_or_falsy_narrowing() {
6619 let errs = errors(
6621 r#"pipeline t(task) {
6622 fn check(x: string | nil, y: int | nil) {
6623 if x || y {
6624 // conservative: can't narrow
6625 } else {
6626 let xn: nil = x
6627 let yn: nil = y
6628 }
6629 }
6630}"#,
6631 );
6632 assert!(errs.is_empty(), "got: {:?}", errs);
6633 }
6634
6635 #[test]
6636 fn test_guard_narrows_outer_scope() {
6637 let errs = errors(
6638 r#"pipeline t(task) {
6639 fn check(x: string | nil) {
6640 guard x != nil else { return }
6641 let s: string = x
6642 }
6643}"#,
6644 );
6645 assert!(errs.is_empty(), "got: {:?}", errs);
6646 }
6647
6648 #[test]
6649 fn test_while_narrows_body() {
6650 let errs = errors(
6651 r#"pipeline t(task) {
6652 fn check(x: string | nil) {
6653 while x != nil {
6654 let s: string = x
6655 break
6656 }
6657 }
6658}"#,
6659 );
6660 assert!(errs.is_empty(), "got: {:?}", errs);
6661 }
6662
6663 #[test]
6664 fn test_early_return_narrows_after_if() {
6665 let errs = errors(
6667 r#"pipeline t(task) {
6668 fn check(x: string | nil) -> string {
6669 if x == nil {
6670 return "default"
6671 }
6672 let s: string = x
6673 return s
6674 }
6675}"#,
6676 );
6677 assert!(errs.is_empty(), "got: {:?}", errs);
6678 }
6679
6680 #[test]
6681 fn test_early_throw_narrows_after_if() {
6682 let errs = errors(
6683 r#"pipeline t(task) {
6684 fn check(x: string | nil) {
6685 if x == nil {
6686 throw "missing"
6687 }
6688 let s: string = x
6689 }
6690}"#,
6691 );
6692 assert!(errs.is_empty(), "got: {:?}", errs);
6693 }
6694
6695 #[test]
6696 fn test_no_narrowing_unknown_type() {
6697 let errs = errors(
6699 r#"pipeline t(task) {
6700 fn check(x) {
6701 if x != nil {
6702 let s: string = x
6703 }
6704 }
6705}"#,
6706 );
6707 assert!(errs.is_empty(), "got: {:?}", errs);
6710 }
6711
6712 #[test]
6713 fn test_reassignment_invalidates_narrowing() {
6714 let errs = errors(
6716 r#"pipeline t(task) {
6717 fn check(x: string | nil) {
6718 var y: string | nil = x
6719 if y != nil {
6720 let s: string = y
6721 y = nil
6722 let s2: string = y
6723 }
6724 }
6725}"#,
6726 );
6727 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
6729 assert!(
6730 errs[0].contains("declared as"),
6731 "expected type mismatch, got: {}",
6732 errs[0]
6733 );
6734 }
6735
6736 #[test]
6737 fn test_let_immutable_warning() {
6738 let all = check_source(
6739 r#"pipeline t(task) {
6740 let x = 42
6741 x = 43
6742}"#,
6743 );
6744 let warnings: Vec<_> = all
6745 .iter()
6746 .filter(|d| d.severity == DiagnosticSeverity::Warning)
6747 .collect();
6748 assert!(
6749 warnings.iter().any(|w| w.message.contains("immutable")),
6750 "expected immutability warning, got: {:?}",
6751 warnings
6752 );
6753 }
6754
6755 #[test]
6756 fn test_nested_narrowing() {
6757 let errs = errors(
6758 r#"pipeline t(task) {
6759 fn check(x: string | int | nil) {
6760 if x != nil {
6761 if type_of(x) == "int" {
6762 let i: int = x
6763 }
6764 }
6765 }
6766}"#,
6767 );
6768 assert!(errs.is_empty(), "got: {:?}", errs);
6769 }
6770
6771 #[test]
6772 fn test_match_narrows_arms() {
6773 let errs = errors(
6774 r#"pipeline t(task) {
6775 fn check(x: string | int) {
6776 match x {
6777 "hello" -> {
6778 let s: string = x
6779 }
6780 42 -> {
6781 let i: int = x
6782 }
6783 _ -> {}
6784 }
6785 }
6786}"#,
6787 );
6788 assert!(errs.is_empty(), "got: {:?}", errs);
6789 }
6790
6791 #[test]
6792 fn test_has_narrows_optional_field() {
6793 let errs = errors(
6794 r#"pipeline t(task) {
6795 fn check(x: {name?: string, age: int}) {
6796 if x.has("name") {
6797 let n: {name: string, age: int} = x
6798 }
6799 }
6800}"#,
6801 );
6802 assert!(errs.is_empty(), "got: {:?}", errs);
6803 }
6804
6805 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
6810 let mut lexer = Lexer::new(source);
6811 let tokens = lexer.tokenize().unwrap();
6812 let mut parser = Parser::new(tokens);
6813 let program = parser.parse().unwrap();
6814 TypeChecker::new().check_with_source(&program, source)
6815 }
6816
6817 #[test]
6818 fn test_fix_string_plus_int_literal() {
6819 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
6820 let diags = check_source_with_source(source);
6821 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
6822 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
6823 let fix = fixable[0].fix.as_ref().unwrap();
6824 assert_eq!(fix.len(), 1);
6825 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
6826 }
6827
6828 #[test]
6829 fn test_fix_int_plus_string_literal() {
6830 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
6831 let diags = check_source_with_source(source);
6832 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
6833 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
6834 let fix = fixable[0].fix.as_ref().unwrap();
6835 assert_eq!(fix[0].replacement, "\"${42}hello\"");
6836 }
6837
6838 #[test]
6839 fn test_fix_string_plus_variable() {
6840 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
6841 let diags = check_source_with_source(source);
6842 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
6843 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
6844 let fix = fixable[0].fix.as_ref().unwrap();
6845 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
6846 }
6847
6848 #[test]
6849 fn test_no_fix_int_plus_int() {
6850 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}";
6852 let diags = check_source_with_source(source);
6853 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
6854 assert!(
6855 fixable.is_empty(),
6856 "no fix expected for numeric ops, got: {fixable:?}"
6857 );
6858 }
6859
6860 #[test]
6861 fn test_no_fix_without_source() {
6862 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
6863 let diags = check_source(source);
6864 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
6865 assert!(
6866 fixable.is_empty(),
6867 "without source, no fix should be generated"
6868 );
6869 }
6870
6871 #[test]
6872 fn test_union_exhaustive_match_no_warning() {
6873 let warns = warnings(
6874 r#"pipeline t(task) {
6875 let x: string | int | nil = nil
6876 match x {
6877 "hello" -> { log("s") }
6878 42 -> { log("i") }
6879 nil -> { log("n") }
6880 }
6881}"#,
6882 );
6883 let union_warns: Vec<_> = warns
6884 .iter()
6885 .filter(|w| w.contains("Non-exhaustive match on union"))
6886 .collect();
6887 assert!(union_warns.is_empty());
6888 }
6889
6890 #[test]
6891 fn test_union_non_exhaustive_match_warning() {
6892 let warns = warnings(
6893 r#"pipeline t(task) {
6894 let x: string | int | nil = nil
6895 match x {
6896 "hello" -> { log("s") }
6897 42 -> { log("i") }
6898 }
6899}"#,
6900 );
6901 let union_warns: Vec<_> = warns
6902 .iter()
6903 .filter(|w| w.contains("Non-exhaustive match on union"))
6904 .collect();
6905 assert_eq!(union_warns.len(), 1);
6906 assert!(union_warns[0].contains("nil"));
6907 }
6908
6909 #[test]
6910 fn test_nil_coalesce_non_union_preserves_left_type() {
6911 let errs = errors(
6913 r#"pipeline t(task) {
6914 let x: int = 42
6915 let y: int = x ?? 0
6916}"#,
6917 );
6918 assert!(errs.is_empty());
6919 }
6920
6921 #[test]
6922 fn test_nil_coalesce_nil_returns_right_type() {
6923 let errs = errors(
6924 r#"pipeline t(task) {
6925 let x: string = nil ?? "fallback"
6926}"#,
6927 );
6928 assert!(errs.is_empty());
6929 }
6930
6931 #[test]
6932 fn test_never_is_subtype_of_everything() {
6933 let tc = TypeChecker::new();
6934 let scope = TypeScope::new();
6935 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &TypeExpr::Never, &scope));
6936 assert!(tc.types_compatible(&TypeExpr::Named("int".into()), &TypeExpr::Never, &scope));
6937 assert!(tc.types_compatible(
6938 &TypeExpr::Union(vec![
6939 TypeExpr::Named("string".into()),
6940 TypeExpr::Named("nil".into()),
6941 ]),
6942 &TypeExpr::Never,
6943 &scope,
6944 ));
6945 }
6946
6947 #[test]
6948 fn test_nothing_is_subtype_of_never() {
6949 let tc = TypeChecker::new();
6950 let scope = TypeScope::new();
6951 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("string".into()), &scope));
6952 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("int".into()), &scope));
6953 }
6954
6955 #[test]
6956 fn test_never_never_compatible() {
6957 let tc = TypeChecker::new();
6958 let scope = TypeScope::new();
6959 assert!(tc.types_compatible(&TypeExpr::Never, &TypeExpr::Never, &scope));
6960 }
6961
6962 #[test]
6963 fn test_any_is_top_type_bidirectional() {
6964 let tc = TypeChecker::new();
6965 let scope = TypeScope::new();
6966 let any = TypeExpr::Named("any".into());
6967 assert!(tc.types_compatible(&any, &TypeExpr::Named("string".into()), &scope));
6969 assert!(tc.types_compatible(&any, &TypeExpr::Named("int".into()), &scope));
6970 assert!(tc.types_compatible(&any, &TypeExpr::Named("nil".into()), &scope));
6971 assert!(tc.types_compatible(
6972 &any,
6973 &TypeExpr::List(Box::new(TypeExpr::Named("int".into()))),
6974 &scope
6975 ));
6976 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &any, &scope));
6978 assert!(tc.types_compatible(&TypeExpr::Named("nil".into()), &any, &scope));
6979 }
6980
6981 #[test]
6982 fn test_unknown_is_safe_top_one_way() {
6983 let tc = TypeChecker::new();
6984 let scope = TypeScope::new();
6985 let unknown = TypeExpr::Named("unknown".into());
6986 assert!(tc.types_compatible(&unknown, &TypeExpr::Named("string".into()), &scope));
6988 assert!(tc.types_compatible(&unknown, &TypeExpr::Named("nil".into()), &scope));
6989 assert!(tc.types_compatible(
6990 &unknown,
6991 &TypeExpr::List(Box::new(TypeExpr::Named("int".into()))),
6992 &scope
6993 ));
6994 assert!(!tc.types_compatible(&TypeExpr::Named("string".into()), &unknown, &scope));
6996 assert!(!tc.types_compatible(&TypeExpr::Named("int".into()), &unknown, &scope));
6997 assert!(tc.types_compatible(&unknown, &unknown, &scope));
6999 assert!(tc.types_compatible(&TypeExpr::Named("any".into()), &unknown, &scope));
7001 }
7002
7003 #[test]
7004 fn test_unknown_narrows_via_type_of() {
7005 let errs = errors(
7010 r#"pipeline t(task) {
7011 fn f(v: unknown) -> string {
7012 if type_of(v) == "string" {
7013 return v
7014 }
7015 return "other"
7016 }
7017 log(f("hi"))
7018}"#,
7019 );
7020 assert!(
7021 errs.is_empty(),
7022 "unknown should narrow to string inside type_of guard: {errs:?}"
7023 );
7024 }
7025
7026 #[test]
7027 fn test_unknown_without_narrowing_errors() {
7028 let errs = errors(
7029 r#"pipeline t(task) {
7030 let u: unknown = "hello"
7031 let s: string = u
7032}"#,
7033 );
7034 assert!(
7035 errs.iter().any(|e| e.contains("unknown")),
7036 "expected an error mentioning unknown, got: {errs:?}"
7037 );
7038 }
7039
7040 #[test]
7041 fn test_simplify_union_removes_never() {
7042 assert_eq!(
7043 simplify_union(vec![TypeExpr::Never, TypeExpr::Named("string".into())]),
7044 TypeExpr::Named("string".into()),
7045 );
7046 assert_eq!(
7047 simplify_union(vec![TypeExpr::Never, TypeExpr::Never]),
7048 TypeExpr::Never,
7049 );
7050 assert_eq!(
7051 simplify_union(vec![
7052 TypeExpr::Named("string".into()),
7053 TypeExpr::Never,
7054 TypeExpr::Named("int".into()),
7055 ]),
7056 TypeExpr::Union(vec![
7057 TypeExpr::Named("string".into()),
7058 TypeExpr::Named("int".into()),
7059 ]),
7060 );
7061 }
7062
7063 #[test]
7064 fn test_remove_from_union_exhausted_returns_never() {
7065 let result = remove_from_union(&[TypeExpr::Named("string".into())], "string");
7066 assert_eq!(result, Some(TypeExpr::Never));
7067 }
7068
7069 #[test]
7070 fn test_if_else_one_branch_throws_infers_other() {
7071 let errs = errors(
7073 r#"pipeline t(task) {
7074 fn foo(x: bool) -> int {
7075 let result: int = if x { 42 } else { throw "err" }
7076 return result
7077 }
7078}"#,
7079 );
7080 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
7081 }
7082
7083 #[test]
7084 fn test_if_else_both_branches_throw_infers_never() {
7085 let errs = errors(
7087 r#"pipeline t(task) {
7088 fn foo(x: bool) -> string {
7089 let result: string = if x { throw "a" } else { throw "b" }
7090 return result
7091 }
7092}"#,
7093 );
7094 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
7095 }
7096
7097 #[test]
7098 fn test_unreachable_after_return() {
7099 let warns = warnings(
7100 r#"pipeline t(task) {
7101 fn foo() -> int {
7102 return 1
7103 let x = 2
7104 }
7105}"#,
7106 );
7107 assert!(
7108 warns.iter().any(|w| w.contains("unreachable")),
7109 "expected unreachable warning: {warns:?}"
7110 );
7111 }
7112
7113 #[test]
7114 fn test_unreachable_after_throw() {
7115 let warns = warnings(
7116 r#"pipeline t(task) {
7117 fn foo() {
7118 throw "err"
7119 let x = 2
7120 }
7121}"#,
7122 );
7123 assert!(
7124 warns.iter().any(|w| w.contains("unreachable")),
7125 "expected unreachable warning: {warns:?}"
7126 );
7127 }
7128
7129 #[test]
7130 fn test_unreachable_after_composite_exit() {
7131 let warns = warnings(
7132 r#"pipeline t(task) {
7133 fn foo(x: bool) {
7134 if x { return 1 } else { throw "err" }
7135 let y = 2
7136 }
7137}"#,
7138 );
7139 assert!(
7140 warns.iter().any(|w| w.contains("unreachable")),
7141 "expected unreachable warning: {warns:?}"
7142 );
7143 }
7144
7145 #[test]
7146 fn test_no_unreachable_warning_when_reachable() {
7147 let warns = warnings(
7148 r#"pipeline t(task) {
7149 fn foo(x: bool) {
7150 if x { return 1 }
7151 let y = 2
7152 }
7153}"#,
7154 );
7155 assert!(
7156 !warns.iter().any(|w| w.contains("unreachable")),
7157 "unexpected unreachable warning: {warns:?}"
7158 );
7159 }
7160
7161 #[test]
7162 fn test_catch_typed_error_variable() {
7163 let errs = errors(
7165 r#"pipeline t(task) {
7166 enum AppError { NotFound, Timeout }
7167 try {
7168 throw AppError.NotFound
7169 } catch (e: AppError) {
7170 let x: AppError = e
7171 }
7172}"#,
7173 );
7174 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
7175 }
7176
7177 #[test]
7178 fn test_unreachable_with_never_arg_no_error() {
7179 let errs = errors(
7181 r#"pipeline t(task) {
7182 fn foo(x: string | int) {
7183 if type_of(x) == "string" { return }
7184 if type_of(x) == "int" { return }
7185 unreachable(x)
7186 }
7187}"#,
7188 );
7189 assert!(
7190 !errs.iter().any(|e| e.contains("unreachable")),
7191 "unexpected unreachable error: {errs:?}"
7192 );
7193 }
7194
7195 #[test]
7196 fn test_unreachable_with_remaining_types_errors() {
7197 let errs = errors(
7199 r#"pipeline t(task) {
7200 fn foo(x: string | int | nil) {
7201 if type_of(x) == "string" { return }
7202 unreachable(x)
7203 }
7204}"#,
7205 );
7206 assert!(
7207 errs.iter()
7208 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
7209 "expected unreachable error about remaining types: {errs:?}"
7210 );
7211 }
7212
7213 #[test]
7214 fn test_unreachable_no_args_no_compile_error() {
7215 let errs = errors(
7216 r#"pipeline t(task) {
7217 fn foo() {
7218 unreachable()
7219 }
7220}"#,
7221 );
7222 assert!(
7223 !errs
7224 .iter()
7225 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
7226 "unreachable() with no args should not produce type error: {errs:?}"
7227 );
7228 }
7229
7230 #[test]
7231 fn test_never_type_annotation_parses() {
7232 let errs = errors(
7233 r#"pipeline t(task) {
7234 fn foo() -> never {
7235 throw "always throws"
7236 }
7237}"#,
7238 );
7239 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
7240 }
7241
7242 #[test]
7243 fn test_format_type_never() {
7244 assert_eq!(format_type(&TypeExpr::Never), "never");
7245 }
7246
7247 fn check_source_strict(source: &str) -> Vec<TypeDiagnostic> {
7250 let mut lexer = Lexer::new(source);
7251 let tokens = lexer.tokenize().unwrap();
7252 let mut parser = Parser::new(tokens);
7253 let program = parser.parse().unwrap();
7254 TypeChecker::with_strict_types(true).check(&program)
7255 }
7256
7257 fn strict_warnings(source: &str) -> Vec<String> {
7258 check_source_strict(source)
7259 .into_iter()
7260 .filter(|d| d.severity == DiagnosticSeverity::Warning)
7261 .map(|d| d.message)
7262 .collect()
7263 }
7264
7265 #[test]
7266 fn test_strict_types_json_parse_property_access() {
7267 let warns = strict_warnings(
7268 r#"pipeline t(task) {
7269 let data = json_parse("{}")
7270 log(data.name)
7271}"#,
7272 );
7273 assert!(
7274 warns.iter().any(|w| w.contains("unvalidated")),
7275 "expected unvalidated warning, got: {warns:?}"
7276 );
7277 }
7278
7279 #[test]
7280 fn test_strict_types_direct_chain_access() {
7281 let warns = strict_warnings(
7282 r#"pipeline t(task) {
7283 log(json_parse("{}").name)
7284}"#,
7285 );
7286 assert!(
7287 warns.iter().any(|w| w.contains("Direct property access")),
7288 "expected direct access warning, got: {warns:?}"
7289 );
7290 }
7291
7292 #[test]
7293 fn test_strict_types_schema_expect_clears() {
7294 let warns = strict_warnings(
7295 r#"pipeline t(task) {
7296 let my_schema = {type: "object", properties: {name: {type: "string"}}}
7297 let data = json_parse("{}")
7298 schema_expect(data, my_schema)
7299 log(data.name)
7300}"#,
7301 );
7302 assert!(
7303 !warns.iter().any(|w| w.contains("unvalidated")),
7304 "expected no unvalidated warning after schema_expect, got: {warns:?}"
7305 );
7306 }
7307
7308 #[test]
7309 fn test_strict_types_schema_is_if_guard() {
7310 let warns = strict_warnings(
7311 r#"pipeline t(task) {
7312 let my_schema = {type: "object", properties: {name: {type: "string"}}}
7313 let data = json_parse("{}")
7314 if schema_is(data, my_schema) {
7315 log(data.name)
7316 }
7317}"#,
7318 );
7319 assert!(
7320 !warns.iter().any(|w| w.contains("unvalidated")),
7321 "expected no unvalidated warning inside schema_is guard, got: {warns:?}"
7322 );
7323 }
7324
7325 #[test]
7326 fn test_strict_types_shape_annotation_clears() {
7327 let warns = strict_warnings(
7328 r#"pipeline t(task) {
7329 let data: {name: string, age: int} = json_parse("{}")
7330 log(data.name)
7331}"#,
7332 );
7333 assert!(
7334 !warns.iter().any(|w| w.contains("unvalidated")),
7335 "expected no warning with shape annotation, got: {warns:?}"
7336 );
7337 }
7338
7339 #[test]
7340 fn test_strict_types_propagation() {
7341 let warns = strict_warnings(
7342 r#"pipeline t(task) {
7343 let data = json_parse("{}")
7344 let x = data
7345 log(x.name)
7346}"#,
7347 );
7348 assert!(
7349 warns
7350 .iter()
7351 .any(|w| w.contains("unvalidated") && w.contains("'x'")),
7352 "expected propagation warning for x, got: {warns:?}"
7353 );
7354 }
7355
7356 #[test]
7357 fn test_strict_types_non_boundary_no_warning() {
7358 let warns = strict_warnings(
7359 r#"pipeline t(task) {
7360 let x = len("hello")
7361 log(x)
7362}"#,
7363 );
7364 assert!(
7365 !warns.iter().any(|w| w.contains("unvalidated")),
7366 "non-boundary function should not be flagged, got: {warns:?}"
7367 );
7368 }
7369
7370 #[test]
7371 fn test_strict_types_subscript_access() {
7372 let warns = strict_warnings(
7373 r#"pipeline t(task) {
7374 let data = json_parse("{}")
7375 log(data["name"])
7376}"#,
7377 );
7378 assert!(
7379 warns.iter().any(|w| w.contains("unvalidated")),
7380 "expected subscript warning, got: {warns:?}"
7381 );
7382 }
7383
7384 #[test]
7385 fn test_strict_types_disabled_by_default() {
7386 let diags = check_source(
7387 r#"pipeline t(task) {
7388 let data = json_parse("{}")
7389 log(data.name)
7390}"#,
7391 );
7392 assert!(
7393 !diags.iter().any(|d| d.message.contains("unvalidated")),
7394 "strict types should be off by default, got: {diags:?}"
7395 );
7396 }
7397
7398 #[test]
7399 fn test_strict_types_llm_call_without_schema() {
7400 let warns = strict_warnings(
7401 r#"pipeline t(task) {
7402 let result = llm_call("prompt", "system")
7403 log(result.text)
7404}"#,
7405 );
7406 assert!(
7407 warns.iter().any(|w| w.contains("unvalidated")),
7408 "llm_call without schema should warn, got: {warns:?}"
7409 );
7410 }
7411
7412 #[test]
7413 fn test_strict_types_llm_call_with_schema_clean() {
7414 let warns = strict_warnings(
7415 r#"pipeline t(task) {
7416 let result = llm_call("prompt", "system", {
7417 schema: {type: "object", properties: {name: {type: "string"}}}
7418 })
7419 log(result.data)
7420 log(result.text)
7421}"#,
7422 );
7423 assert!(
7424 !warns.iter().any(|w| w.contains("unvalidated")),
7425 "llm_call with schema should not warn, got: {warns:?}"
7426 );
7427 }
7428
7429 #[test]
7430 fn test_strict_types_schema_expect_result_typed() {
7431 let warns = strict_warnings(
7432 r#"pipeline t(task) {
7433 let my_schema = {type: "object", properties: {name: {type: "string"}}}
7434 let validated = schema_expect(json_parse("{}"), my_schema)
7435 log(validated.name)
7436}"#,
7437 );
7438 assert!(
7439 !warns.iter().any(|w| w.contains("unvalidated")),
7440 "schema_expect result should be typed, got: {warns:?}"
7441 );
7442 }
7443
7444 #[test]
7445 fn test_strict_types_realistic_orchestration() {
7446 let warns = strict_warnings(
7447 r#"pipeline t(task) {
7448 let payload_schema = {type: "object", properties: {
7449 name: {type: "string"},
7450 steps: {type: "list", items: {type: "string"}}
7451 }}
7452
7453 // Good: schema-aware llm_call
7454 let result = llm_call("generate a workflow", "system", {
7455 schema: payload_schema
7456 })
7457 let workflow_name = result.data.name
7458
7459 // Good: validate then access
7460 let raw = json_parse("{}")
7461 schema_expect(raw, payload_schema)
7462 let steps = raw.steps
7463
7464 log(workflow_name)
7465 log(steps)
7466}"#,
7467 );
7468 assert!(
7469 !warns.iter().any(|w| w.contains("unvalidated")),
7470 "validated orchestration should be clean, got: {warns:?}"
7471 );
7472 }
7473
7474 #[test]
7475 fn test_strict_types_llm_call_with_schema_via_variable() {
7476 let warns = strict_warnings(
7477 r#"pipeline t(task) {
7478 let my_schema = {type: "object", properties: {score: {type: "float"}}}
7479 let result = llm_call("rate this", "system", {
7480 schema: my_schema
7481 })
7482 log(result.data.score)
7483}"#,
7484 );
7485 assert!(
7486 !warns.iter().any(|w| w.contains("unvalidated")),
7487 "llm_call with schema variable should not warn, got: {warns:?}"
7488 );
7489 }
7490}