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