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