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