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