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)]
38struct EnumDeclInfo {
39 type_params: Vec<String>,
40 variants: Vec<EnumVariant>,
41}
42
43#[derive(Debug, Clone)]
44struct StructDeclInfo {
45 type_params: Vec<String>,
46 fields: Vec<StructField>,
47}
48
49#[derive(Debug, Clone)]
50struct InterfaceDeclInfo {
51 type_params: Vec<String>,
52 associated_types: Vec<(String, Option<TypeExpr>)>,
53 methods: Vec<InterfaceMethod>,
54}
55
56#[derive(Debug, Clone)]
58struct TypeScope {
59 vars: BTreeMap<String, InferredType>,
61 functions: BTreeMap<String, FnSignature>,
63 type_aliases: BTreeMap<String, TypeExpr>,
65 enums: BTreeMap<String, EnumDeclInfo>,
67 interfaces: BTreeMap<String, InterfaceDeclInfo>,
69 structs: BTreeMap<String, StructDeclInfo>,
71 impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
73 generic_type_params: std::collections::BTreeSet<String>,
75 where_constraints: BTreeMap<String, String>,
78 mutable_vars: std::collections::BTreeSet<String>,
81 narrowed_vars: BTreeMap<String, InferredType>,
84 schema_bindings: BTreeMap<String, InferredType>,
87 untyped_sources: BTreeMap<String, String>,
91 parent: Option<Box<TypeScope>>,
92}
93
94#[derive(Debug, Clone)]
96struct ImplMethodSig {
97 name: String,
98 param_count: usize,
100 param_types: Vec<Option<TypeExpr>>,
102 return_type: Option<TypeExpr>,
104}
105
106#[derive(Debug, Clone)]
107struct FnSignature {
108 params: Vec<(String, InferredType)>,
109 return_type: InferredType,
110 type_param_names: Vec<String>,
112 required_params: usize,
114 where_clauses: Vec<(String, String)>,
116 has_rest: bool,
118}
119
120impl TypeScope {
121 fn new() -> Self {
122 let mut scope = Self {
123 vars: BTreeMap::new(),
124 functions: BTreeMap::new(),
125 type_aliases: BTreeMap::new(),
126 enums: BTreeMap::new(),
127 interfaces: BTreeMap::new(),
128 structs: BTreeMap::new(),
129 impl_methods: BTreeMap::new(),
130 generic_type_params: std::collections::BTreeSet::new(),
131 where_constraints: BTreeMap::new(),
132 mutable_vars: std::collections::BTreeSet::new(),
133 narrowed_vars: BTreeMap::new(),
134 schema_bindings: BTreeMap::new(),
135 untyped_sources: BTreeMap::new(),
136 parent: None,
137 };
138 scope.enums.insert(
139 "Result".into(),
140 EnumDeclInfo {
141 type_params: vec!["T".into(), "E".into()],
142 variants: vec![
143 EnumVariant {
144 name: "Ok".into(),
145 fields: vec![TypedParam {
146 name: "value".into(),
147 type_expr: Some(TypeExpr::Named("T".into())),
148 default_value: None,
149 rest: false,
150 }],
151 },
152 EnumVariant {
153 name: "Err".into(),
154 fields: vec![TypedParam {
155 name: "error".into(),
156 type_expr: Some(TypeExpr::Named("E".into())),
157 default_value: None,
158 rest: false,
159 }],
160 },
161 ],
162 },
163 );
164 scope
165 }
166
167 fn child(&self) -> Self {
168 Self {
169 vars: BTreeMap::new(),
170 functions: BTreeMap::new(),
171 type_aliases: BTreeMap::new(),
172 enums: BTreeMap::new(),
173 interfaces: BTreeMap::new(),
174 structs: BTreeMap::new(),
175 impl_methods: BTreeMap::new(),
176 generic_type_params: std::collections::BTreeSet::new(),
177 where_constraints: BTreeMap::new(),
178 mutable_vars: std::collections::BTreeSet::new(),
179 narrowed_vars: BTreeMap::new(),
180 schema_bindings: BTreeMap::new(),
181 untyped_sources: BTreeMap::new(),
182 parent: Some(Box::new(self.clone())),
183 }
184 }
185
186 fn get_var(&self, name: &str) -> Option<&InferredType> {
187 self.vars
188 .get(name)
189 .or_else(|| self.parent.as_ref()?.get_var(name))
190 }
191
192 fn get_fn(&self, name: &str) -> Option<&FnSignature> {
193 self.functions
194 .get(name)
195 .or_else(|| self.parent.as_ref()?.get_fn(name))
196 }
197
198 fn get_schema_binding(&self, name: &str) -> Option<&InferredType> {
199 self.schema_bindings
200 .get(name)
201 .or_else(|| self.parent.as_ref()?.get_schema_binding(name))
202 }
203
204 fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
205 self.type_aliases
206 .get(name)
207 .or_else(|| self.parent.as_ref()?.resolve_type(name))
208 }
209
210 fn is_generic_type_param(&self, name: &str) -> bool {
211 self.generic_type_params.contains(name)
212 || self
213 .parent
214 .as_ref()
215 .is_some_and(|p| p.is_generic_type_param(name))
216 }
217
218 fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
219 self.where_constraints
220 .get(type_param)
221 .map(|s| s.as_str())
222 .or_else(|| {
223 self.parent
224 .as_ref()
225 .and_then(|p| p.get_where_constraint(type_param))
226 })
227 }
228
229 fn get_enum(&self, name: &str) -> Option<&EnumDeclInfo> {
230 self.enums
231 .get(name)
232 .or_else(|| self.parent.as_ref()?.get_enum(name))
233 }
234
235 fn get_interface(&self, name: &str) -> Option<&InterfaceDeclInfo> {
236 self.interfaces
237 .get(name)
238 .or_else(|| self.parent.as_ref()?.get_interface(name))
239 }
240
241 fn get_struct(&self, name: &str) -> Option<&StructDeclInfo> {
242 self.structs
243 .get(name)
244 .or_else(|| self.parent.as_ref()?.get_struct(name))
245 }
246
247 fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
248 self.impl_methods
249 .get(name)
250 .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
251 }
252
253 fn define_var(&mut self, name: &str, ty: InferredType) {
254 self.vars.insert(name.to_string(), ty);
255 }
256
257 fn define_var_mutable(&mut self, name: &str, ty: InferredType) {
258 self.vars.insert(name.to_string(), ty);
259 self.mutable_vars.insert(name.to_string());
260 }
261
262 fn define_schema_binding(&mut self, name: &str, ty: InferredType) {
263 self.schema_bindings.insert(name.to_string(), ty);
264 }
265
266 fn is_untyped_source(&self, name: &str) -> Option<&str> {
269 if let Some(source) = self.untyped_sources.get(name) {
270 if source.is_empty() {
271 return None; }
273 return Some(source.as_str());
274 }
275 self.parent.as_ref()?.is_untyped_source(name)
276 }
277
278 fn mark_untyped_source(&mut self, name: &str, source: &str) {
279 self.untyped_sources
280 .insert(name.to_string(), source.to_string());
281 }
282
283 fn clear_untyped_source(&mut self, name: &str) {
285 self.untyped_sources.insert(name.to_string(), String::new());
286 }
287
288 fn is_mutable(&self, name: &str) -> bool {
290 self.mutable_vars.contains(name) || self.parent.as_ref().is_some_and(|p| p.is_mutable(name))
291 }
292
293 fn define_fn(&mut self, name: &str, sig: FnSignature) {
294 self.functions.insert(name.to_string(), sig);
295 }
296}
297
298#[derive(Debug, Clone, Default)]
301struct Refinements {
302 truthy: Vec<(String, InferredType)>,
304 falsy: Vec<(String, InferredType)>,
306}
307
308impl Refinements {
309 fn empty() -> Self {
310 Self::default()
311 }
312
313 fn inverted(self) -> Self {
315 Self {
316 truthy: self.falsy,
317 falsy: self.truthy,
318 }
319 }
320}
321
322fn builtin_return_type(name: &str) -> InferredType {
325 builtin_signatures::builtin_return_type(name)
326}
327
328fn is_builtin(name: &str) -> bool {
331 builtin_signatures::is_builtin(name)
332}
333
334pub struct TypeChecker {
336 diagnostics: Vec<TypeDiagnostic>,
337 scope: TypeScope,
338 source: Option<String>,
339 hints: Vec<InlayHintInfo>,
340 strict_types: bool,
342}
343
344impl TypeChecker {
345 fn wildcard_type() -> TypeExpr {
346 TypeExpr::Named("_".into())
347 }
348
349 fn is_wildcard_type(ty: &TypeExpr) -> bool {
350 matches!(ty, TypeExpr::Named(name) if name == "_")
351 }
352
353 fn base_type_name(ty: &TypeExpr) -> Option<&str> {
354 match ty {
355 TypeExpr::Named(name) => Some(name.as_str()),
356 TypeExpr::Applied { name, .. } => Some(name.as_str()),
357 _ => None,
358 }
359 }
360
361 pub fn new() -> Self {
362 Self {
363 diagnostics: Vec::new(),
364 scope: TypeScope::new(),
365 source: None,
366 hints: Vec::new(),
367 strict_types: false,
368 }
369 }
370
371 pub fn with_strict_types(strict: bool) -> Self {
374 Self {
375 diagnostics: Vec::new(),
376 scope: TypeScope::new(),
377 source: None,
378 hints: Vec::new(),
379 strict_types: strict,
380 }
381 }
382
383 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
385 self.source = Some(source.to_string());
386 self.check_inner(program).0
387 }
388
389 pub fn check_strict_with_source(
391 mut self,
392 program: &[SNode],
393 source: &str,
394 ) -> Vec<TypeDiagnostic> {
395 self.source = Some(source.to_string());
396 self.check_inner(program).0
397 }
398
399 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
401 self.check_inner(program).0
402 }
403
404 fn detect_boundary_source(value: &SNode, scope: &TypeScope) -> Option<String> {
408 match &value.node {
409 Node::FunctionCall { name, args } => {
410 if !builtin_signatures::is_untyped_boundary_source(name) {
411 return None;
412 }
413 if (name == "llm_call" || name == "llm_completion")
415 && Self::extract_llm_schema_from_options(args, scope).is_some()
416 {
417 return None;
418 }
419 Some(name.clone())
420 }
421 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
422 _ => None,
423 }
424 }
425
426 fn extract_llm_schema_from_options(args: &[SNode], scope: &TypeScope) -> Option<TypeExpr> {
429 let opts = args.get(2)?;
430 let entries = match &opts.node {
431 Node::DictLiteral(entries) => entries,
432 _ => return None,
433 };
434 for entry in entries {
435 let key = match &entry.key.node {
436 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
437 _ => continue,
438 };
439 if key == "schema" || key == "output_schema" {
440 return schema_type_expr_from_node(&entry.value, scope);
441 }
442 }
443 None
444 }
445
446 fn is_concrete_type(ty: &TypeExpr) -> bool {
449 matches!(
450 ty,
451 TypeExpr::Shape(_)
452 | TypeExpr::Applied { .. }
453 | TypeExpr::FnType { .. }
454 | TypeExpr::List(_)
455 | TypeExpr::Iter(_)
456 | TypeExpr::DictType(_, _)
457 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
458 }
459
460 pub fn check_with_hints(
462 mut self,
463 program: &[SNode],
464 source: &str,
465 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
466 self.source = Some(source.to_string());
467 self.check_inner(program)
468 }
469
470 fn check_inner(mut self, program: &[SNode]) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
471 Self::register_declarations_into(&mut self.scope, program);
473
474 for snode in program {
476 if let Node::Pipeline { body, .. } = &snode.node {
477 Self::register_declarations_into(&mut self.scope, body);
478 }
479 }
480
481 for snode in program {
483 match &snode.node {
484 Node::Pipeline { params, body, .. } => {
485 let mut child = self.scope.child();
486 for p in params {
487 child.define_var(p, None);
488 }
489 self.check_block(body, &mut child);
490 }
491 Node::FnDecl {
492 name,
493 type_params,
494 params,
495 return_type,
496 where_clauses,
497 body,
498 ..
499 } => {
500 let required_params =
501 params.iter().filter(|p| p.default_value.is_none()).count();
502 let sig = FnSignature {
503 params: params
504 .iter()
505 .map(|p| (p.name.clone(), p.type_expr.clone()))
506 .collect(),
507 return_type: return_type.clone(),
508 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
509 required_params,
510 where_clauses: where_clauses
511 .iter()
512 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
513 .collect(),
514 has_rest: params.last().is_some_and(|p| p.rest),
515 };
516 self.scope.define_fn(name, sig);
517 self.check_fn_body(type_params, params, return_type, body, where_clauses);
518 }
519 _ => {
520 let mut scope = self.scope.clone();
521 self.check_node(snode, &mut scope);
522 for (name, ty) in scope.vars {
524 self.scope.vars.entry(name).or_insert(ty);
525 }
526 for name in scope.mutable_vars {
527 self.scope.mutable_vars.insert(name);
528 }
529 }
530 }
531 }
532
533 (self.diagnostics, self.hints)
534 }
535
536 fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
538 for snode in nodes {
539 match &snode.node {
540 Node::TypeDecl { name, type_expr } => {
541 scope.type_aliases.insert(name.clone(), type_expr.clone());
542 }
543 Node::EnumDecl {
544 name,
545 type_params,
546 variants,
547 ..
548 } => {
549 scope.enums.insert(
550 name.clone(),
551 EnumDeclInfo {
552 type_params: type_params.iter().map(|tp| tp.name.clone()).collect(),
553 variants: variants.clone(),
554 },
555 );
556 }
557 Node::InterfaceDecl {
558 name,
559 type_params,
560 associated_types,
561 methods,
562 } => {
563 scope.interfaces.insert(
564 name.clone(),
565 InterfaceDeclInfo {
566 type_params: type_params.iter().map(|tp| tp.name.clone()).collect(),
567 associated_types: associated_types.clone(),
568 methods: methods.clone(),
569 },
570 );
571 }
572 Node::StructDecl {
573 name,
574 type_params,
575 fields,
576 ..
577 } => {
578 scope.structs.insert(
579 name.clone(),
580 StructDeclInfo {
581 type_params: type_params.iter().map(|tp| tp.name.clone()).collect(),
582 fields: fields.clone(),
583 },
584 );
585 }
586 Node::ImplBlock {
587 type_name, methods, ..
588 } => {
589 let sigs: Vec<ImplMethodSig> = methods
590 .iter()
591 .filter_map(|m| {
592 if let Node::FnDecl {
593 name,
594 params,
595 return_type,
596 ..
597 } = &m.node
598 {
599 let non_self: Vec<_> =
600 params.iter().filter(|p| p.name != "self").collect();
601 let param_count = non_self.len();
602 let param_types: Vec<Option<TypeExpr>> =
603 non_self.iter().map(|p| p.type_expr.clone()).collect();
604 Some(ImplMethodSig {
605 name: name.clone(),
606 param_count,
607 param_types,
608 return_type: return_type.clone(),
609 })
610 } else {
611 None
612 }
613 })
614 .collect();
615 scope.impl_methods.insert(type_name.clone(), sigs);
616 }
617 _ => {}
618 }
619 }
620 }
621
622 fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
623 let mut definitely_exited = false;
624 for stmt in stmts {
625 if definitely_exited {
626 self.warning_at("unreachable code".to_string(), stmt.span);
627 break; }
629 self.check_node(stmt, scope);
630 if Self::stmt_definitely_exits(stmt) {
631 definitely_exited = true;
632 }
633 }
634 }
635
636 fn stmt_definitely_exits(stmt: &SNode) -> bool {
638 stmt_definitely_exits(stmt)
639 }
640
641 fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope, mutable: bool) {
643 let define = |scope: &mut TypeScope, name: &str| {
644 if mutable {
645 scope.define_var_mutable(name, None);
646 } else {
647 scope.define_var(name, None);
648 }
649 };
650 match pattern {
651 BindingPattern::Identifier(name) => {
652 define(scope, name);
653 }
654 BindingPattern::Dict(fields) => {
655 for field in fields {
656 let name = field.alias.as_deref().unwrap_or(&field.key);
657 define(scope, name);
658 }
659 }
660 BindingPattern::List(elements) => {
661 for elem in elements {
662 define(scope, &elem.name);
663 }
664 }
665 BindingPattern::Pair(a, b) => {
666 define(scope, a);
667 define(scope, b);
668 }
669 }
670 }
671
672 fn check_pattern_defaults(&mut self, pattern: &BindingPattern, scope: &mut TypeScope) {
674 match pattern {
675 BindingPattern::Identifier(_) => {}
676 BindingPattern::Dict(fields) => {
677 for field in fields {
678 if let Some(default) = &field.default_value {
679 self.check_binops(default, scope);
680 }
681 }
682 }
683 BindingPattern::List(elements) => {
684 for elem in elements {
685 if let Some(default) = &elem.default_value {
686 self.check_binops(default, scope);
687 }
688 }
689 }
690 BindingPattern::Pair(_, _) => {}
691 }
692 }
693
694 fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
695 let span = snode.span;
696 match &snode.node {
697 Node::LetBinding {
698 pattern,
699 type_ann,
700 value,
701 } => {
702 self.check_binops(value, scope);
703 let inferred = self.infer_type(value, scope);
704 if let BindingPattern::Identifier(name) = pattern {
705 if let Some(expected) = type_ann {
706 if let Some(actual) = &inferred {
707 if !self.types_compatible(expected, actual, scope) {
708 let mut msg = format!(
709 "'{}' declared as {}, but assigned {}",
710 name,
711 format_type(expected),
712 format_type(actual)
713 );
714 if let Some(detail) = shape_mismatch_detail(expected, actual) {
715 msg.push_str(&format!(" ({})", detail));
716 }
717 self.error_at(msg, span);
718 }
719 }
720 }
721 if type_ann.is_none() {
723 if let Some(ref ty) = inferred {
724 if !is_obvious_type(value, ty) {
725 self.hints.push(InlayHintInfo {
726 line: span.line,
727 column: span.column + "let ".len() + name.len(),
728 label: format!(": {}", format_type(ty)),
729 });
730 }
731 }
732 }
733 let ty = type_ann.clone().or(inferred);
734 scope.define_var(name, ty);
735 scope.define_schema_binding(name, schema_type_expr_from_node(value, scope));
736 if self.strict_types {
738 if let Some(boundary) = Self::detect_boundary_source(value, scope) {
739 let has_concrete_ann =
740 type_ann.as_ref().is_some_and(Self::is_concrete_type);
741 if !has_concrete_ann {
742 scope.mark_untyped_source(name, &boundary);
743 }
744 }
745 }
746 } else {
747 self.check_pattern_defaults(pattern, scope);
748 Self::define_pattern_vars(pattern, scope, false);
749 }
750 }
751
752 Node::VarBinding {
753 pattern,
754 type_ann,
755 value,
756 } => {
757 self.check_binops(value, scope);
758 let inferred = self.infer_type(value, scope);
759 if let BindingPattern::Identifier(name) = pattern {
760 if let Some(expected) = type_ann {
761 if let Some(actual) = &inferred {
762 if !self.types_compatible(expected, actual, scope) {
763 let mut msg = format!(
764 "'{}' declared as {}, but assigned {}",
765 name,
766 format_type(expected),
767 format_type(actual)
768 );
769 if let Some(detail) = shape_mismatch_detail(expected, actual) {
770 msg.push_str(&format!(" ({})", detail));
771 }
772 self.error_at(msg, span);
773 }
774 }
775 }
776 if type_ann.is_none() {
777 if let Some(ref ty) = inferred {
778 if !is_obvious_type(value, ty) {
779 self.hints.push(InlayHintInfo {
780 line: span.line,
781 column: span.column + "var ".len() + name.len(),
782 label: format!(": {}", format_type(ty)),
783 });
784 }
785 }
786 }
787 let ty = type_ann.clone().or(inferred);
788 scope.define_var_mutable(name, ty);
789 scope.define_schema_binding(name, schema_type_expr_from_node(value, scope));
790 if self.strict_types {
792 if let Some(boundary) = Self::detect_boundary_source(value, scope) {
793 let has_concrete_ann =
794 type_ann.as_ref().is_some_and(Self::is_concrete_type);
795 if !has_concrete_ann {
796 scope.mark_untyped_source(name, &boundary);
797 }
798 }
799 }
800 } else {
801 self.check_pattern_defaults(pattern, scope);
802 Self::define_pattern_vars(pattern, scope, true);
803 }
804 }
805
806 Node::FnDecl {
807 name,
808 type_params,
809 params,
810 return_type,
811 where_clauses,
812 body,
813 ..
814 } => {
815 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
816 let sig = FnSignature {
817 params: params
818 .iter()
819 .map(|p| (p.name.clone(), p.type_expr.clone()))
820 .collect(),
821 return_type: return_type.clone(),
822 type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
823 required_params,
824 where_clauses: where_clauses
825 .iter()
826 .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
827 .collect(),
828 has_rest: params.last().is_some_and(|p| p.rest),
829 };
830 scope.define_fn(name, sig.clone());
831 scope.define_var(name, None);
832 self.check_fn_body(type_params, params, return_type, body, where_clauses);
833 }
834
835 Node::ToolDecl {
836 name,
837 params,
838 return_type,
839 body,
840 ..
841 } => {
842 let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
844 let sig = FnSignature {
845 params: params
846 .iter()
847 .map(|p| (p.name.clone(), p.type_expr.clone()))
848 .collect(),
849 return_type: return_type.clone(),
850 type_param_names: Vec::new(),
851 required_params,
852 where_clauses: Vec::new(),
853 has_rest: params.last().is_some_and(|p| p.rest),
854 };
855 scope.define_fn(name, sig);
856 scope.define_var(name, None);
857 self.check_fn_body(&[], params, return_type, body, &[]);
858 }
859
860 Node::FunctionCall { name, args } => {
861 self.check_call(name, args, scope, span);
862 if self.strict_types && name == "schema_expect" && args.len() >= 2 {
864 if let Node::Identifier(var_name) = &args[0].node {
865 scope.clear_untyped_source(var_name);
866 if let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) {
867 scope.define_var(var_name, Some(schema_type));
868 }
869 }
870 }
871 }
872
873 Node::IfElse {
874 condition,
875 then_body,
876 else_body,
877 } => {
878 self.check_node(condition, scope);
879 let refs = Self::extract_refinements(condition, scope);
880
881 let mut then_scope = scope.child();
882 apply_refinements(&mut then_scope, &refs.truthy);
883 if self.strict_types {
886 if let Node::FunctionCall { name, args } = &condition.node {
887 if (name == "schema_is" || name == "is_type") && args.len() == 2 {
888 if let Node::Identifier(var_name) = &args[0].node {
889 then_scope.clear_untyped_source(var_name);
890 }
891 }
892 }
893 }
894 self.check_block(then_body, &mut then_scope);
895
896 if let Some(else_body) = else_body {
897 let mut else_scope = scope.child();
898 apply_refinements(&mut else_scope, &refs.falsy);
899 self.check_block(else_body, &mut else_scope);
900
901 if Self::block_definitely_exits(then_body)
904 && !Self::block_definitely_exits(else_body)
905 {
906 apply_refinements(scope, &refs.falsy);
907 } else if Self::block_definitely_exits(else_body)
908 && !Self::block_definitely_exits(then_body)
909 {
910 apply_refinements(scope, &refs.truthy);
911 }
912 } else {
913 if Self::block_definitely_exits(then_body) {
915 apply_refinements(scope, &refs.falsy);
916 }
917 }
918 }
919
920 Node::ForIn {
921 pattern,
922 iterable,
923 body,
924 } => {
925 self.check_node(iterable, scope);
926 let mut loop_scope = scope.child();
927 let iter_type = self.infer_type(iterable, scope);
928 if let BindingPattern::Identifier(variable) = pattern {
929 let elem_type = match iter_type {
931 Some(TypeExpr::List(inner)) => Some(*inner),
932 Some(TypeExpr::Iter(inner)) => Some(*inner),
933 Some(TypeExpr::Applied { ref name, ref args })
934 if name == "Iter" && args.len() == 1 =>
935 {
936 Some(args[0].clone())
937 }
938 Some(TypeExpr::Named(n)) if n == "string" => {
939 Some(TypeExpr::Named("string".into()))
940 }
941 Some(TypeExpr::Named(n)) if n == "range" => {
943 Some(TypeExpr::Named("int".into()))
944 }
945 _ => None,
946 };
947 loop_scope.define_var(variable, elem_type);
948 } else if let BindingPattern::Pair(a, b) = pattern {
949 let (ka, vb) = match &iter_type {
952 Some(TypeExpr::Iter(inner)) => {
953 if let TypeExpr::Applied { name, args } = inner.as_ref() {
954 if name == "Pair" && args.len() == 2 {
955 (Some(args[0].clone()), Some(args[1].clone()))
956 } else {
957 (None, None)
958 }
959 } else {
960 (None, None)
961 }
962 }
963 Some(TypeExpr::Applied { name, args })
964 if name == "Iter" && args.len() == 1 =>
965 {
966 if let TypeExpr::Applied { name: n2, args: a2 } = &args[0] {
967 if n2 == "Pair" && a2.len() == 2 {
968 (Some(a2[0].clone()), Some(a2[1].clone()))
969 } else {
970 (None, None)
971 }
972 } else {
973 (None, None)
974 }
975 }
976 _ => (None, None),
977 };
978 loop_scope.define_var(a, ka);
979 loop_scope.define_var(b, vb);
980 } else {
981 self.check_pattern_defaults(pattern, &mut loop_scope);
982 Self::define_pattern_vars(pattern, &mut loop_scope, false);
983 }
984 self.check_block(body, &mut loop_scope);
985 }
986
987 Node::WhileLoop { condition, body } => {
988 self.check_node(condition, scope);
989 let refs = Self::extract_refinements(condition, scope);
990 let mut loop_scope = scope.child();
991 apply_refinements(&mut loop_scope, &refs.truthy);
992 self.check_block(body, &mut loop_scope);
993 }
994
995 Node::RequireStmt { condition, message } => {
996 self.check_node(condition, scope);
997 if let Some(message) = message {
998 self.check_node(message, scope);
999 }
1000 }
1001
1002 Node::TryCatch {
1003 body,
1004 error_var,
1005 error_type,
1006 catch_body,
1007 finally_body,
1008 ..
1009 } => {
1010 let mut try_scope = scope.child();
1011 self.check_block(body, &mut try_scope);
1012 let mut catch_scope = scope.child();
1013 if let Some(var) = error_var {
1014 catch_scope.define_var(var, error_type.clone());
1015 }
1016 self.check_block(catch_body, &mut catch_scope);
1017 if let Some(fb) = finally_body {
1018 let mut finally_scope = scope.child();
1019 self.check_block(fb, &mut finally_scope);
1020 }
1021 }
1022
1023 Node::TryExpr { body } => {
1024 let mut try_scope = scope.child();
1025 self.check_block(body, &mut try_scope);
1026 }
1027
1028 Node::ReturnStmt {
1029 value: Some(val), ..
1030 } => {
1031 self.check_node(val, scope);
1032 }
1033
1034 Node::Assignment {
1035 target, value, op, ..
1036 } => {
1037 self.check_node(value, scope);
1038 if let Node::Identifier(name) = &target.node {
1039 if scope.get_var(name).is_some() && !scope.is_mutable(name) {
1041 self.warning_at(
1042 format!(
1043 "Cannot assign to '{}': variable is immutable (declared with 'let')",
1044 name
1045 ),
1046 span,
1047 );
1048 }
1049
1050 if let Some(Some(var_type)) = scope.get_var(name) {
1051 let value_type = self.infer_type(value, scope);
1052 let assigned = if let Some(op) = op {
1053 let var_inferred = scope.get_var(name).cloned().flatten();
1054 infer_binary_op_type(op, &var_inferred, &value_type)
1055 } else {
1056 value_type
1057 };
1058 if let Some(actual) = &assigned {
1059 let check_type = scope
1061 .narrowed_vars
1062 .get(name)
1063 .and_then(|t| t.as_ref())
1064 .unwrap_or(var_type);
1065 if !self.types_compatible(check_type, actual, scope) {
1066 self.error_at(
1067 format!(
1068 "can't assign {} to '{}' (declared as {})",
1069 format_type(actual),
1070 name,
1071 format_type(check_type)
1072 ),
1073 span,
1074 );
1075 }
1076 }
1077 }
1078
1079 if let Some(original) = scope.narrowed_vars.remove(name) {
1081 scope.define_var(name, original);
1082 }
1083 scope.define_schema_binding(name, None);
1084 }
1085 }
1086
1087 Node::TypeDecl { name, type_expr } => {
1088 scope.type_aliases.insert(name.clone(), type_expr.clone());
1089 }
1090
1091 Node::EnumDecl {
1092 name,
1093 type_params,
1094 variants,
1095 ..
1096 } => {
1097 scope.enums.insert(
1098 name.clone(),
1099 EnumDeclInfo {
1100 type_params: type_params.iter().map(|tp| tp.name.clone()).collect(),
1101 variants: variants.clone(),
1102 },
1103 );
1104 }
1105
1106 Node::StructDecl {
1107 name,
1108 type_params,
1109 fields,
1110 ..
1111 } => {
1112 scope.structs.insert(
1113 name.clone(),
1114 StructDeclInfo {
1115 type_params: type_params.iter().map(|tp| tp.name.clone()).collect(),
1116 fields: fields.clone(),
1117 },
1118 );
1119 }
1120
1121 Node::InterfaceDecl {
1122 name,
1123 type_params,
1124 associated_types,
1125 methods,
1126 } => {
1127 scope.interfaces.insert(
1128 name.clone(),
1129 InterfaceDeclInfo {
1130 type_params: type_params.iter().map(|tp| tp.name.clone()).collect(),
1131 associated_types: associated_types.clone(),
1132 methods: methods.clone(),
1133 },
1134 );
1135 }
1136
1137 Node::ImplBlock {
1138 type_name, methods, ..
1139 } => {
1140 let sigs: Vec<ImplMethodSig> = methods
1142 .iter()
1143 .filter_map(|m| {
1144 if let Node::FnDecl {
1145 name,
1146 params,
1147 return_type,
1148 ..
1149 } = &m.node
1150 {
1151 let non_self: Vec<_> =
1152 params.iter().filter(|p| p.name != "self").collect();
1153 let param_count = non_self.len();
1154 let param_types: Vec<Option<TypeExpr>> =
1155 non_self.iter().map(|p| p.type_expr.clone()).collect();
1156 Some(ImplMethodSig {
1157 name: name.clone(),
1158 param_count,
1159 param_types,
1160 return_type: return_type.clone(),
1161 })
1162 } else {
1163 None
1164 }
1165 })
1166 .collect();
1167 scope.impl_methods.insert(type_name.clone(), sigs);
1168 for method_sn in methods {
1169 self.check_node(method_sn, scope);
1170 }
1171 }
1172
1173 Node::TryOperator { operand } => {
1174 self.check_node(operand, scope);
1175 }
1176
1177 Node::MatchExpr { value, arms } => {
1178 self.check_node(value, scope);
1179 let value_type = self.infer_type(value, scope);
1180 for arm in arms {
1181 self.check_node(&arm.pattern, scope);
1182 if let Some(ref vt) = value_type {
1184 let value_type_name = format_type(vt);
1185 let mismatch = match &arm.pattern.node {
1186 Node::StringLiteral(_) => {
1187 !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
1188 }
1189 Node::IntLiteral(_) => {
1190 !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
1191 && !self.types_compatible(
1192 vt,
1193 &TypeExpr::Named("float".into()),
1194 scope,
1195 )
1196 }
1197 Node::FloatLiteral(_) => {
1198 !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
1199 && !self.types_compatible(
1200 vt,
1201 &TypeExpr::Named("int".into()),
1202 scope,
1203 )
1204 }
1205 Node::BoolLiteral(_) => {
1206 !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
1207 }
1208 _ => false,
1209 };
1210 if mismatch {
1211 let pattern_type = match &arm.pattern.node {
1212 Node::StringLiteral(_) => "string",
1213 Node::IntLiteral(_) => "int",
1214 Node::FloatLiteral(_) => "float",
1215 Node::BoolLiteral(_) => "bool",
1216 _ => unreachable!(),
1217 };
1218 self.warning_at(
1219 format!(
1220 "Match pattern type mismatch: matching {} against {} literal",
1221 value_type_name, pattern_type
1222 ),
1223 arm.pattern.span,
1224 );
1225 }
1226 }
1227 let mut arm_scope = scope.child();
1228 if let Node::Identifier(var_name) = &value.node {
1230 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(var_name) {
1231 let narrowed = match &arm.pattern.node {
1232 Node::NilLiteral => narrow_to_single(members, "nil"),
1233 Node::StringLiteral(_) => narrow_to_single(members, "string"),
1234 Node::IntLiteral(_) => narrow_to_single(members, "int"),
1235 Node::FloatLiteral(_) => narrow_to_single(members, "float"),
1236 Node::BoolLiteral(_) => narrow_to_single(members, "bool"),
1237 _ => None,
1238 };
1239 if let Some(narrowed_type) = narrowed {
1240 arm_scope.define_var(var_name, Some(narrowed_type));
1241 }
1242 }
1243 }
1244 if let Some(ref guard) = arm.guard {
1245 self.check_node(guard, &mut arm_scope);
1246 }
1247 self.check_block(&arm.body, &mut arm_scope);
1248 }
1249 self.check_match_exhaustiveness(value, arms, scope, span);
1250 }
1251
1252 Node::BinaryOp { op, left, right } => {
1254 self.check_node(left, scope);
1255 self.check_node(right, scope);
1256 let lt = self.infer_type(left, scope);
1258 let rt = self.infer_type(right, scope);
1259 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
1260 match op.as_str() {
1261 "-" | "/" | "%" | "**" => {
1262 let numeric = ["int", "float"];
1263 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
1264 self.error_at(
1265 format!(
1266 "can't use '{}' on {} and {} (needs numeric operands)",
1267 op, l, r
1268 ),
1269 span,
1270 );
1271 }
1272 }
1273 "*" => {
1274 let numeric = ["int", "float"];
1275 let is_numeric =
1276 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
1277 let is_string_repeat =
1278 (l == "string" && r == "int") || (l == "int" && r == "string");
1279 if !is_numeric && !is_string_repeat {
1280 self.error_at(
1281 format!("can't multiply {} and {} (try string * int)", l, r),
1282 span,
1283 );
1284 }
1285 }
1286 "+" => {
1287 let valid = matches!(
1288 (l.as_str(), r.as_str()),
1289 ("int" | "float", "int" | "float")
1290 | ("string", "string")
1291 | ("list", "list")
1292 | ("dict", "dict")
1293 );
1294 if !valid {
1295 let msg = format!("can't add {} and {}", l, r);
1296 let fix = if l == "string" || r == "string" {
1298 self.build_interpolation_fix(left, right, l == "string", span)
1299 } else {
1300 None
1301 };
1302 if let Some(fix) = fix {
1303 self.error_at_with_fix(msg, span, fix);
1304 } else {
1305 self.error_at(msg, span);
1306 }
1307 }
1308 }
1309 "<" | ">" | "<=" | ">=" => {
1310 let comparable = ["int", "float", "string"];
1311 if !comparable.contains(&l.as_str())
1312 || !comparable.contains(&r.as_str())
1313 {
1314 self.warning_at(
1315 format!(
1316 "Comparison '{}' may not be meaningful for types {} and {}",
1317 op, l, r
1318 ),
1319 span,
1320 );
1321 } else if (l == "string") != (r == "string") {
1322 self.warning_at(
1323 format!(
1324 "Comparing {} with {} using '{}' may give unexpected results",
1325 l, r, op
1326 ),
1327 span,
1328 );
1329 }
1330 }
1331 _ => {}
1332 }
1333 }
1334 }
1335 Node::UnaryOp { operand, .. } => {
1336 self.check_node(operand, scope);
1337 }
1338 Node::MethodCall {
1339 object,
1340 method,
1341 args,
1342 ..
1343 }
1344 | Node::OptionalMethodCall {
1345 object,
1346 method,
1347 args,
1348 ..
1349 } => {
1350 self.check_node(object, scope);
1351 for arg in args {
1352 self.check_node(arg, scope);
1353 }
1354 if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
1358 if scope.is_generic_type_param(&type_name) {
1359 if let Some(iface_name) = scope.get_where_constraint(&type_name) {
1360 if let Some(iface_methods) = scope.get_interface(iface_name) {
1361 let has_method =
1362 iface_methods.methods.iter().any(|m| m.name == *method);
1363 if !has_method {
1364 self.warning_at(
1365 format!(
1366 "Method '{}' not found in interface '{}' (constraint on '{}')",
1367 method, iface_name, type_name
1368 ),
1369 span,
1370 );
1371 }
1372 }
1373 }
1374 }
1375 }
1376 }
1377 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1378 if self.strict_types {
1379 if let Node::FunctionCall { name, args } = &object.node {
1381 if builtin_signatures::is_untyped_boundary_source(name) {
1382 let has_schema = (name == "llm_call" || name == "llm_completion")
1383 && Self::extract_llm_schema_from_options(args, scope).is_some();
1384 if !has_schema {
1385 self.warning_at_with_help(
1386 format!(
1387 "Direct property access on unvalidated `{}()` result",
1388 name
1389 ),
1390 span,
1391 "assign to a variable and validate with schema_expect() or a type annotation first".to_string(),
1392 );
1393 }
1394 }
1395 }
1396 if let Node::Identifier(name) = &object.node {
1398 if let Some(source) = scope.is_untyped_source(name) {
1399 self.warning_at_with_help(
1400 format!(
1401 "Accessing property on unvalidated value '{}' from `{}`",
1402 name, source
1403 ),
1404 span,
1405 "validate with schema_expect(), schema_is() in an if-condition, or add a shape type annotation".to_string(),
1406 );
1407 }
1408 }
1409 }
1410 self.check_node(object, scope);
1411 }
1412 Node::SubscriptAccess { object, index } => {
1413 if self.strict_types {
1414 if let Node::FunctionCall { name, args } = &object.node {
1415 if builtin_signatures::is_untyped_boundary_source(name) {
1416 let has_schema = (name == "llm_call" || name == "llm_completion")
1417 && Self::extract_llm_schema_from_options(args, scope).is_some();
1418 if !has_schema {
1419 self.warning_at_with_help(
1420 format!(
1421 "Direct subscript access on unvalidated `{}()` result",
1422 name
1423 ),
1424 span,
1425 "assign to a variable and validate with schema_expect() or a type annotation first".to_string(),
1426 );
1427 }
1428 }
1429 }
1430 if let Node::Identifier(name) = &object.node {
1431 if let Some(source) = scope.is_untyped_source(name) {
1432 self.warning_at_with_help(
1433 format!(
1434 "Subscript access on unvalidated value '{}' from `{}`",
1435 name, source
1436 ),
1437 span,
1438 "validate with schema_expect(), schema_is() in an if-condition, or add a shape type annotation".to_string(),
1439 );
1440 }
1441 }
1442 }
1443 self.check_node(object, scope);
1444 self.check_node(index, scope);
1445 }
1446 Node::SliceAccess { object, start, end } => {
1447 self.check_node(object, scope);
1448 if let Some(s) = start {
1449 self.check_node(s, scope);
1450 }
1451 if let Some(e) = end {
1452 self.check_node(e, scope);
1453 }
1454 }
1455
1456 Node::Ternary {
1458 condition,
1459 true_expr,
1460 false_expr,
1461 } => {
1462 self.check_node(condition, scope);
1463 let refs = Self::extract_refinements(condition, scope);
1464
1465 let mut true_scope = scope.child();
1466 apply_refinements(&mut true_scope, &refs.truthy);
1467 self.check_node(true_expr, &mut true_scope);
1468
1469 let mut false_scope = scope.child();
1470 apply_refinements(&mut false_scope, &refs.falsy);
1471 self.check_node(false_expr, &mut false_scope);
1472 }
1473
1474 Node::ThrowStmt { value } => {
1475 self.check_node(value, scope);
1476 }
1477
1478 Node::GuardStmt {
1479 condition,
1480 else_body,
1481 } => {
1482 self.check_node(condition, scope);
1483 let refs = Self::extract_refinements(condition, scope);
1484
1485 let mut else_scope = scope.child();
1486 apply_refinements(&mut else_scope, &refs.falsy);
1487 self.check_block(else_body, &mut else_scope);
1488
1489 apply_refinements(scope, &refs.truthy);
1492 }
1493
1494 Node::SpawnExpr { body } => {
1495 let mut spawn_scope = scope.child();
1496 self.check_block(body, &mut spawn_scope);
1497 }
1498
1499 Node::Parallel {
1500 mode,
1501 expr,
1502 variable,
1503 body,
1504 options,
1505 } => {
1506 self.check_node(expr, scope);
1507 for (key, value) in options {
1508 self.check_node(value, scope);
1513 if key == "max_concurrent" {
1514 if let Some(ty) = self.infer_type(value, scope) {
1515 if !matches!(ty, TypeExpr::Named(ref n) if n == "int") {
1516 self.error_at(
1517 format!(
1518 "`max_concurrent` on `parallel` must be int, got {ty:?}"
1519 ),
1520 value.span,
1521 );
1522 }
1523 }
1524 }
1525 }
1526 let mut par_scope = scope.child();
1527 if let Some(var) = variable {
1528 let var_type = match mode {
1529 ParallelMode::Count => Some(TypeExpr::Named("int".into())),
1530 ParallelMode::Each | ParallelMode::Settle => {
1531 match self.infer_type(expr, scope) {
1532 Some(TypeExpr::List(inner)) => Some(*inner),
1533 _ => None,
1534 }
1535 }
1536 };
1537 par_scope.define_var(var, var_type);
1538 }
1539 self.check_block(body, &mut par_scope);
1540 }
1541
1542 Node::SelectExpr {
1543 cases,
1544 timeout,
1545 default_body,
1546 } => {
1547 for case in cases {
1548 self.check_node(&case.channel, scope);
1549 let mut case_scope = scope.child();
1550 case_scope.define_var(&case.variable, None);
1551 self.check_block(&case.body, &mut case_scope);
1552 }
1553 if let Some((dur, body)) = timeout {
1554 self.check_node(dur, scope);
1555 let mut timeout_scope = scope.child();
1556 self.check_block(body, &mut timeout_scope);
1557 }
1558 if let Some(body) = default_body {
1559 let mut default_scope = scope.child();
1560 self.check_block(body, &mut default_scope);
1561 }
1562 }
1563
1564 Node::DeadlineBlock { duration, body } => {
1565 self.check_node(duration, scope);
1566 let mut block_scope = scope.child();
1567 self.check_block(body, &mut block_scope);
1568 }
1569
1570 Node::MutexBlock { body } | Node::DeferStmt { body } => {
1571 let mut block_scope = scope.child();
1572 self.check_block(body, &mut block_scope);
1573 }
1574
1575 Node::Retry { count, body } => {
1576 self.check_node(count, scope);
1577 let mut retry_scope = scope.child();
1578 self.check_block(body, &mut retry_scope);
1579 }
1580
1581 Node::Closure { params, body, .. } => {
1582 let mut closure_scope = scope.child();
1583 for p in params {
1584 closure_scope.define_var(&p.name, p.type_expr.clone());
1585 }
1586 self.check_block(body, &mut closure_scope);
1587 }
1588
1589 Node::ListLiteral(elements) => {
1590 for elem in elements {
1591 self.check_node(elem, scope);
1592 }
1593 }
1594
1595 Node::DictLiteral(entries) => {
1596 for entry in entries {
1597 self.check_node(&entry.key, scope);
1598 self.check_node(&entry.value, scope);
1599 }
1600 }
1601
1602 Node::RangeExpr { start, end, .. } => {
1603 self.check_node(start, scope);
1604 self.check_node(end, scope);
1605 }
1606
1607 Node::Spread(inner) => {
1608 self.check_node(inner, scope);
1609 }
1610
1611 Node::Block(stmts) => {
1612 let mut block_scope = scope.child();
1613 self.check_block(stmts, &mut block_scope);
1614 }
1615
1616 Node::YieldExpr { value } => {
1617 if let Some(v) = value {
1618 self.check_node(v, scope);
1619 }
1620 }
1621
1622 Node::StructConstruct {
1624 struct_name,
1625 fields,
1626 } => {
1627 for entry in fields {
1628 self.check_node(&entry.key, scope);
1629 self.check_node(&entry.value, scope);
1630 }
1631 if let Some(struct_info) = scope.get_struct(struct_name).cloned() {
1632 let type_bindings = self.infer_struct_bindings(&struct_info, fields, scope);
1633 for entry in fields {
1635 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1636 if !struct_info.fields.iter().any(|field| field.name == *key) {
1637 self.warning_at(
1638 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1639 entry.key.span,
1640 );
1641 }
1642 }
1643 }
1644 let provided: Vec<String> = fields
1646 .iter()
1647 .filter_map(|e| match &e.key.node {
1648 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1649 _ => None,
1650 })
1651 .collect();
1652 for field in &struct_info.fields {
1653 if !field.optional && !provided.contains(&field.name) {
1654 self.warning_at(
1655 format!(
1656 "Missing field '{}' in struct '{}' construction",
1657 field.name, struct_name
1658 ),
1659 span,
1660 );
1661 }
1662 }
1663 for field in &struct_info.fields {
1664 let Some(expected_type) = &field.type_expr else {
1665 continue;
1666 };
1667 let Some(entry) = fields.iter().find(|entry| {
1668 matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
1669 }) else {
1670 continue;
1671 };
1672 let Some(actual_type) = self.infer_type(&entry.value, scope) else {
1673 continue;
1674 };
1675 let expected = Self::apply_type_bindings(expected_type, &type_bindings);
1676 if !self.types_compatible(&expected, &actual_type, scope) {
1677 self.error_at(
1678 format!(
1679 "Field '{}' in struct '{}' expects {}, got {}",
1680 field.name,
1681 struct_name,
1682 format_type(&expected),
1683 format_type(&actual_type)
1684 ),
1685 entry.value.span,
1686 );
1687 }
1688 }
1689 }
1690 }
1691
1692 Node::EnumConstruct {
1694 enum_name,
1695 variant,
1696 args,
1697 } => {
1698 for arg in args {
1699 self.check_node(arg, scope);
1700 }
1701 if let Some(enum_info) = scope.get_enum(enum_name).cloned() {
1702 let Some(enum_variant) = enum_info
1703 .variants
1704 .iter()
1705 .find(|enum_variant| enum_variant.name == *variant)
1706 else {
1707 self.warning_at(
1708 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1709 span,
1710 );
1711 return;
1712 };
1713 if args.len() != enum_variant.fields.len() {
1714 self.warning_at(
1715 format!(
1716 "{}.{} expects {} argument(s), got {}",
1717 enum_name,
1718 variant,
1719 enum_variant.fields.len(),
1720 args.len()
1721 ),
1722 span,
1723 );
1724 }
1725 let type_param_set: std::collections::BTreeSet<String> =
1726 enum_info.type_params.iter().cloned().collect();
1727 let mut type_bindings = BTreeMap::new();
1728 for (field, arg) in enum_variant.fields.iter().zip(args.iter()) {
1729 let Some(expected_type) = &field.type_expr else {
1730 continue;
1731 };
1732 let Some(actual_type) = self.infer_type(arg, scope) else {
1733 continue;
1734 };
1735 if let Err(message) = Self::extract_type_bindings(
1736 expected_type,
1737 &actual_type,
1738 &type_param_set,
1739 &mut type_bindings,
1740 ) {
1741 self.error_at(message, arg.span);
1742 }
1743 }
1744 for (field, arg) in enum_variant.fields.iter().zip(args.iter()) {
1745 let Some(expected_type) = &field.type_expr else {
1746 continue;
1747 };
1748 let Some(actual_type) = self.infer_type(arg, scope) else {
1749 continue;
1750 };
1751 let expected = Self::apply_type_bindings(expected_type, &type_bindings);
1752 if !self.types_compatible(&expected, &actual_type, scope) {
1753 self.error_at(
1754 format!(
1755 "{}.{} expects {}: {}, got {}",
1756 enum_name,
1757 variant,
1758 field.name,
1759 format_type(&expected),
1760 format_type(&actual_type)
1761 ),
1762 arg.span,
1763 );
1764 }
1765 }
1766 }
1767 }
1768
1769 Node::InterpolatedString(_) => {}
1771
1772 Node::StringLiteral(_)
1774 | Node::RawStringLiteral(_)
1775 | Node::IntLiteral(_)
1776 | Node::FloatLiteral(_)
1777 | Node::BoolLiteral(_)
1778 | Node::NilLiteral
1779 | Node::Identifier(_)
1780 | Node::DurationLiteral(_)
1781 | Node::BreakStmt
1782 | Node::ContinueStmt
1783 | Node::ReturnStmt { value: None }
1784 | Node::ImportDecl { .. }
1785 | Node::SelectiveImport { .. } => {}
1786
1787 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1790 let mut decl_scope = scope.child();
1791 self.check_block(body, &mut decl_scope);
1792 }
1793 }
1794 }
1795
1796 fn check_fn_body(
1797 &mut self,
1798 type_params: &[TypeParam],
1799 params: &[TypedParam],
1800 return_type: &Option<TypeExpr>,
1801 body: &[SNode],
1802 where_clauses: &[WhereClause],
1803 ) {
1804 let mut fn_scope = self.scope.child();
1805 for tp in type_params {
1808 fn_scope.generic_type_params.insert(tp.name.clone());
1809 }
1810 for wc in where_clauses {
1812 fn_scope
1813 .where_constraints
1814 .insert(wc.type_name.clone(), wc.bound.clone());
1815 }
1816 for param in params {
1817 fn_scope.define_var(¶m.name, param.type_expr.clone());
1818 if let Some(default) = ¶m.default_value {
1819 self.check_node(default, &mut fn_scope);
1820 }
1821 }
1822 let ret_scope_base = if return_type.is_some() {
1825 Some(fn_scope.child())
1826 } else {
1827 None
1828 };
1829
1830 self.check_block(body, &mut fn_scope);
1831
1832 if let Some(ret_type) = return_type {
1834 let mut ret_scope = ret_scope_base.unwrap();
1835 for stmt in body {
1836 self.check_return_type(stmt, ret_type, &mut ret_scope);
1837 }
1838 }
1839 }
1840
1841 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1842 let span = snode.span;
1843 match &snode.node {
1844 Node::ReturnStmt { value: Some(val) } => {
1845 let inferred = self.infer_type(val, scope);
1846 if let Some(actual) = &inferred {
1847 if !self.types_compatible(expected, actual, scope) {
1848 self.error_at(
1849 format!(
1850 "return type doesn't match: expected {}, got {}",
1851 format_type(expected),
1852 format_type(actual)
1853 ),
1854 span,
1855 );
1856 }
1857 }
1858 }
1859 Node::IfElse {
1860 condition,
1861 then_body,
1862 else_body,
1863 } => {
1864 let refs = Self::extract_refinements(condition, scope);
1865 let mut then_scope = scope.child();
1866 apply_refinements(&mut then_scope, &refs.truthy);
1867 for stmt in then_body {
1868 self.check_return_type(stmt, expected, &mut then_scope);
1869 }
1870 if let Some(else_body) = else_body {
1871 let mut else_scope = scope.child();
1872 apply_refinements(&mut else_scope, &refs.falsy);
1873 for stmt in else_body {
1874 self.check_return_type(stmt, expected, &mut else_scope);
1875 }
1876 if Self::block_definitely_exits(then_body)
1878 && !Self::block_definitely_exits(else_body)
1879 {
1880 apply_refinements(scope, &refs.falsy);
1881 } else if Self::block_definitely_exits(else_body)
1882 && !Self::block_definitely_exits(then_body)
1883 {
1884 apply_refinements(scope, &refs.truthy);
1885 }
1886 } else {
1887 if Self::block_definitely_exits(then_body) {
1889 apply_refinements(scope, &refs.falsy);
1890 }
1891 }
1892 }
1893 _ => {}
1894 }
1895 }
1896
1897 fn satisfies_interface(
1903 &self,
1904 type_name: &str,
1905 interface_name: &str,
1906 interface_bindings: &BTreeMap<String, TypeExpr>,
1907 scope: &TypeScope,
1908 ) -> bool {
1909 self.interface_mismatch_reason(type_name, interface_name, interface_bindings, scope)
1910 .is_none()
1911 }
1912
1913 fn interface_mismatch_reason(
1916 &self,
1917 type_name: &str,
1918 interface_name: &str,
1919 interface_bindings: &BTreeMap<String, TypeExpr>,
1920 scope: &TypeScope,
1921 ) -> Option<String> {
1922 let interface_info = match scope.get_interface(interface_name) {
1923 Some(info) => info,
1924 None => return Some(format!("interface '{}' not found", interface_name)),
1925 };
1926 let impl_methods = match scope.get_impl_methods(type_name) {
1927 Some(methods) => methods,
1928 None => {
1929 if interface_info.methods.is_empty() {
1930 return None;
1931 }
1932 let names: Vec<_> = interface_info
1933 .methods
1934 .iter()
1935 .map(|m| m.name.as_str())
1936 .collect();
1937 return Some(format!("missing method(s): {}", names.join(", ")));
1938 }
1939 };
1940 let mut bindings = interface_bindings.clone();
1941 let associated_type_names: std::collections::BTreeSet<String> = interface_info
1942 .associated_types
1943 .iter()
1944 .map(|(name, _)| name.clone())
1945 .collect();
1946 for iface_method in &interface_info.methods {
1947 let iface_params: Vec<_> = iface_method
1948 .params
1949 .iter()
1950 .filter(|p| p.name != "self")
1951 .collect();
1952 let iface_param_count = iface_params.len();
1953 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1954 let impl_method = match matching_impl {
1955 Some(m) => m,
1956 None => {
1957 return Some(format!("missing method '{}'", iface_method.name));
1958 }
1959 };
1960 if impl_method.param_count != iface_param_count {
1961 return Some(format!(
1962 "method '{}' has {} parameter(s), expected {}",
1963 iface_method.name, impl_method.param_count, iface_param_count
1964 ));
1965 }
1966 for (i, iface_param) in iface_params.iter().enumerate() {
1968 if let (Some(expected), Some(actual)) = (
1969 &iface_param.type_expr,
1970 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1971 ) {
1972 if let Err(message) = Self::extract_type_bindings(
1973 expected,
1974 actual,
1975 &associated_type_names,
1976 &mut bindings,
1977 ) {
1978 return Some(message);
1979 }
1980 let expected = Self::apply_type_bindings(expected, &bindings);
1981 if !self.types_compatible(&expected, actual, scope) {
1982 return Some(format!(
1983 "method '{}' parameter {} has type '{}', expected '{}'",
1984 iface_method.name,
1985 i + 1,
1986 format_type(actual),
1987 format_type(&expected),
1988 ));
1989 }
1990 }
1991 }
1992 if let (Some(expected_ret), Some(actual_ret)) =
1994 (&iface_method.return_type, &impl_method.return_type)
1995 {
1996 if let Err(message) = Self::extract_type_bindings(
1997 expected_ret,
1998 actual_ret,
1999 &associated_type_names,
2000 &mut bindings,
2001 ) {
2002 return Some(message);
2003 }
2004 let expected_ret = Self::apply_type_bindings(expected_ret, &bindings);
2005 if !self.types_compatible(&expected_ret, actual_ret, scope) {
2006 return Some(format!(
2007 "method '{}' returns '{}', expected '{}'",
2008 iface_method.name,
2009 format_type(actual_ret),
2010 format_type(&expected_ret),
2011 ));
2012 }
2013 }
2014 }
2015 for (assoc_name, default_type) in &interface_info.associated_types {
2016 if let (Some(default_type), Some(actual)) = (default_type, bindings.get(assoc_name)) {
2017 let expected = Self::apply_type_bindings(default_type, &bindings);
2018 if !self.types_compatible(&expected, actual, scope) {
2019 return Some(format!(
2020 "associated type '{}' resolves to '{}', expected '{}'",
2021 assoc_name,
2022 format_type(actual),
2023 format_type(&expected),
2024 ));
2025 }
2026 }
2027 }
2028 None
2029 }
2030
2031 fn bind_type_param(
2032 param_name: &str,
2033 concrete: &TypeExpr,
2034 bindings: &mut BTreeMap<String, TypeExpr>,
2035 ) -> Result<(), String> {
2036 if let Some(existing) = bindings.get(param_name) {
2037 if existing != concrete {
2038 return Err(format!(
2039 "type parameter '{}' was inferred as both {} and {}",
2040 param_name,
2041 format_type(existing),
2042 format_type(concrete)
2043 ));
2044 }
2045 return Ok(());
2046 }
2047 bindings.insert(param_name.to_string(), concrete.clone());
2048 Ok(())
2049 }
2050
2051 fn extract_type_bindings(
2054 param_type: &TypeExpr,
2055 arg_type: &TypeExpr,
2056 type_params: &std::collections::BTreeSet<String>,
2057 bindings: &mut BTreeMap<String, TypeExpr>,
2058 ) -> Result<(), String> {
2059 match (param_type, arg_type) {
2060 (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
2061 Self::bind_type_param(param_name, concrete, bindings)
2062 }
2063 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
2064 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
2065 }
2066 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
2067 Self::extract_type_bindings(pk, ak, type_params, bindings)?;
2068 Self::extract_type_bindings(pv, av, type_params, bindings)
2069 }
2070 (
2071 TypeExpr::Applied {
2072 name: p_name,
2073 args: p_args,
2074 },
2075 TypeExpr::Applied {
2076 name: a_name,
2077 args: a_args,
2078 },
2079 ) if p_name == a_name && p_args.len() == a_args.len() => {
2080 for (param, arg) in p_args.iter().zip(a_args.iter()) {
2081 Self::extract_type_bindings(param, arg, type_params, bindings)?;
2082 }
2083 Ok(())
2084 }
2085 (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
2086 for param_field in param_fields {
2087 if let Some(arg_field) = arg_fields
2088 .iter()
2089 .find(|field| field.name == param_field.name)
2090 {
2091 Self::extract_type_bindings(
2092 ¶m_field.type_expr,
2093 &arg_field.type_expr,
2094 type_params,
2095 bindings,
2096 )?;
2097 }
2098 }
2099 Ok(())
2100 }
2101 (
2102 TypeExpr::FnType {
2103 params: p_params,
2104 return_type: p_ret,
2105 },
2106 TypeExpr::FnType {
2107 params: a_params,
2108 return_type: a_ret,
2109 },
2110 ) => {
2111 for (param, arg) in p_params.iter().zip(a_params.iter()) {
2112 Self::extract_type_bindings(param, arg, type_params, bindings)?;
2113 }
2114 Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
2115 }
2116 _ => Ok(()),
2117 }
2118 }
2119
2120 fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
2121 match ty {
2122 TypeExpr::Named(name) => bindings
2123 .get(name)
2124 .cloned()
2125 .unwrap_or_else(|| TypeExpr::Named(name.clone())),
2126 TypeExpr::Union(items) => TypeExpr::Union(
2127 items
2128 .iter()
2129 .map(|item| Self::apply_type_bindings(item, bindings))
2130 .collect(),
2131 ),
2132 TypeExpr::Shape(fields) => TypeExpr::Shape(
2133 fields
2134 .iter()
2135 .map(|field| ShapeField {
2136 name: field.name.clone(),
2137 type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
2138 optional: field.optional,
2139 })
2140 .collect(),
2141 ),
2142 TypeExpr::List(inner) => {
2143 TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
2144 }
2145 TypeExpr::Iter(inner) => {
2146 TypeExpr::Iter(Box::new(Self::apply_type_bindings(inner, bindings)))
2147 }
2148 TypeExpr::DictType(key, value) => TypeExpr::DictType(
2149 Box::new(Self::apply_type_bindings(key, bindings)),
2150 Box::new(Self::apply_type_bindings(value, bindings)),
2151 ),
2152 TypeExpr::Applied { name, args } => TypeExpr::Applied {
2153 name: name.clone(),
2154 args: args
2155 .iter()
2156 .map(|arg| Self::apply_type_bindings(arg, bindings))
2157 .collect(),
2158 },
2159 TypeExpr::FnType {
2160 params,
2161 return_type,
2162 } => TypeExpr::FnType {
2163 params: params
2164 .iter()
2165 .map(|param| Self::apply_type_bindings(param, bindings))
2166 .collect(),
2167 return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
2168 },
2169 TypeExpr::Never => TypeExpr::Never,
2170 }
2171 }
2172
2173 fn applied_type_or_name(name: &str, args: Vec<TypeExpr>) -> TypeExpr {
2174 if args.is_empty() {
2175 TypeExpr::Named(name.to_string())
2176 } else {
2177 TypeExpr::Applied {
2178 name: name.to_string(),
2179 args,
2180 }
2181 }
2182 }
2183
2184 fn infer_struct_bindings(
2185 &self,
2186 struct_info: &StructDeclInfo,
2187 fields: &[DictEntry],
2188 scope: &TypeScope,
2189 ) -> BTreeMap<String, TypeExpr> {
2190 let type_param_set: std::collections::BTreeSet<String> =
2191 struct_info.type_params.iter().cloned().collect();
2192 let mut bindings = BTreeMap::new();
2193 for field in &struct_info.fields {
2194 let Some(expected_type) = &field.type_expr else {
2195 continue;
2196 };
2197 let Some(entry) = fields.iter().find(|entry| {
2198 matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
2199 }) else {
2200 continue;
2201 };
2202 let Some(actual_type) = self.infer_type(&entry.value, scope) else {
2203 continue;
2204 };
2205 let _ = Self::extract_type_bindings(
2206 expected_type,
2207 &actual_type,
2208 &type_param_set,
2209 &mut bindings,
2210 );
2211 }
2212 bindings
2213 }
2214
2215 fn infer_struct_type(
2216 &self,
2217 struct_name: &str,
2218 struct_info: &StructDeclInfo,
2219 fields: &[DictEntry],
2220 scope: &TypeScope,
2221 ) -> TypeExpr {
2222 let bindings = self.infer_struct_bindings(struct_info, fields, scope);
2223 let args = struct_info
2224 .type_params
2225 .iter()
2226 .map(|name| {
2227 bindings
2228 .get(name)
2229 .cloned()
2230 .unwrap_or_else(Self::wildcard_type)
2231 })
2232 .collect();
2233 Self::applied_type_or_name(struct_name, args)
2234 }
2235
2236 fn infer_enum_type(
2237 &self,
2238 enum_name: &str,
2239 enum_info: &EnumDeclInfo,
2240 variant_name: &str,
2241 args: &[SNode],
2242 scope: &TypeScope,
2243 ) -> TypeExpr {
2244 let type_param_set: std::collections::BTreeSet<String> =
2245 enum_info.type_params.iter().cloned().collect();
2246 let mut bindings = BTreeMap::new();
2247 if let Some(variant) = enum_info
2248 .variants
2249 .iter()
2250 .find(|variant| variant.name == variant_name)
2251 {
2252 for (field, arg) in variant.fields.iter().zip(args.iter()) {
2253 let Some(expected_type) = &field.type_expr else {
2254 continue;
2255 };
2256 let Some(actual_type) = self.infer_type(arg, scope) else {
2257 continue;
2258 };
2259 let _ = Self::extract_type_bindings(
2260 expected_type,
2261 &actual_type,
2262 &type_param_set,
2263 &mut bindings,
2264 );
2265 }
2266 }
2267 let args = enum_info
2268 .type_params
2269 .iter()
2270 .map(|name| {
2271 bindings
2272 .get(name)
2273 .cloned()
2274 .unwrap_or_else(Self::wildcard_type)
2275 })
2276 .collect();
2277 Self::applied_type_or_name(enum_name, args)
2278 }
2279
2280 fn infer_try_error_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
2281 let mut inferred: Vec<TypeExpr> = Vec::new();
2282 for stmt in stmts {
2283 match &stmt.node {
2284 Node::ThrowStmt { value } => {
2285 if let Some(ty) = self.infer_type(value, scope) {
2286 inferred.push(ty);
2287 }
2288 }
2289 Node::TryOperator { operand } => {
2290 if let Some(TypeExpr::Applied { name, args }) = self.infer_type(operand, scope)
2291 {
2292 if name == "Result" && args.len() == 2 {
2293 inferred.push(args[1].clone());
2294 }
2295 }
2296 }
2297 Node::IfElse {
2298 then_body,
2299 else_body,
2300 ..
2301 } => {
2302 if let Some(ty) = self.infer_try_error_type(then_body, scope) {
2303 inferred.push(ty);
2304 }
2305 if let Some(else_body) = else_body {
2306 if let Some(ty) = self.infer_try_error_type(else_body, scope) {
2307 inferred.push(ty);
2308 }
2309 }
2310 }
2311 Node::Block(body)
2312 | Node::TryExpr { body }
2313 | Node::SpawnExpr { body }
2314 | Node::Retry { body, .. }
2315 | Node::WhileLoop { body, .. }
2316 | Node::DeferStmt { body }
2317 | Node::MutexBlock { body }
2318 | Node::DeadlineBlock { body, .. }
2319 | Node::Pipeline { body, .. }
2320 | Node::OverrideDecl { body, .. } => {
2321 if let Some(ty) = self.infer_try_error_type(body, scope) {
2322 inferred.push(ty);
2323 }
2324 }
2325 _ => {}
2326 }
2327 }
2328 if inferred.is_empty() {
2329 None
2330 } else {
2331 Some(simplify_union(inferred))
2332 }
2333 }
2334
2335 fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
2336 let mut inferred: Option<TypeExpr> = None;
2337 for item in items {
2338 let Some(item_type) = self.infer_type(item, scope) else {
2339 return TypeExpr::Named("list".into());
2340 };
2341 inferred = Some(match inferred {
2342 None => item_type,
2343 Some(current) if current == item_type => current,
2344 Some(TypeExpr::Union(mut members)) => {
2345 if !members.contains(&item_type) {
2346 members.push(item_type);
2347 }
2348 TypeExpr::Union(members)
2349 }
2350 Some(current) => TypeExpr::Union(vec![current, item_type]),
2351 });
2352 }
2353 inferred
2354 .map(|item_type| TypeExpr::List(Box::new(item_type)))
2355 .unwrap_or_else(|| TypeExpr::Named("list".into()))
2356 }
2357
2358 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
2360 match &condition.node {
2361 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
2363 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
2364 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
2365 return nil_ref;
2366 }
2367 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
2368 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
2369 return typeof_ref;
2370 }
2371 Refinements::empty()
2372 }
2373
2374 Node::BinaryOp { op, left, right } if op == "&&" => {
2376 let left_ref = Self::extract_refinements(left, scope);
2377 let right_ref = Self::extract_refinements(right, scope);
2378 let mut truthy = left_ref.truthy;
2379 truthy.extend(right_ref.truthy);
2380 Refinements {
2381 truthy,
2382 falsy: vec![],
2383 }
2384 }
2385
2386 Node::BinaryOp { op, left, right } if op == "||" => {
2388 let left_ref = Self::extract_refinements(left, scope);
2389 let right_ref = Self::extract_refinements(right, scope);
2390 let mut falsy = left_ref.falsy;
2391 falsy.extend(right_ref.falsy);
2392 Refinements {
2393 truthy: vec![],
2394 falsy,
2395 }
2396 }
2397
2398 Node::UnaryOp { op, operand } if op == "!" => {
2400 Self::extract_refinements(operand, scope).inverted()
2401 }
2402
2403 Node::Identifier(name) => {
2405 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
2406 if members
2407 .iter()
2408 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
2409 {
2410 if let Some(narrowed) = remove_from_union(members, "nil") {
2411 return Refinements {
2412 truthy: vec![(name.clone(), Some(narrowed))],
2413 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2414 };
2415 }
2416 }
2417 }
2418 Refinements::empty()
2419 }
2420
2421 Node::MethodCall {
2423 object,
2424 method,
2425 args,
2426 } if method == "has" && args.len() == 1 => {
2427 Self::extract_has_refinements(object, args, scope)
2428 }
2429
2430 Node::FunctionCall { name, args }
2431 if (name == "schema_is" || name == "is_type") && args.len() == 2 =>
2432 {
2433 Self::extract_schema_refinements(args, scope)
2434 }
2435
2436 _ => Refinements::empty(),
2437 }
2438 }
2439
2440 fn extract_nil_refinements(
2442 op: &str,
2443 left: &SNode,
2444 right: &SNode,
2445 scope: &TypeScope,
2446 ) -> Refinements {
2447 let var_node = if matches!(right.node, Node::NilLiteral) {
2448 left
2449 } else if matches!(left.node, Node::NilLiteral) {
2450 right
2451 } else {
2452 return Refinements::empty();
2453 };
2454
2455 if let Node::Identifier(name) = &var_node.node {
2456 let var_type = scope.get_var(name).cloned().flatten();
2457 match var_type {
2458 Some(TypeExpr::Union(ref members)) => {
2459 if let Some(narrowed) = remove_from_union(members, "nil") {
2460 let neq_refs = Refinements {
2461 truthy: vec![(name.clone(), Some(narrowed))],
2462 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2463 };
2464 return if op == "!=" {
2465 neq_refs
2466 } else {
2467 neq_refs.inverted()
2468 };
2469 }
2470 }
2471 Some(TypeExpr::Named(ref n)) if n == "nil" => {
2472 let eq_refs = Refinements {
2474 truthy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2475 falsy: vec![(name.clone(), Some(TypeExpr::Never))],
2476 };
2477 return if op == "==" {
2478 eq_refs
2479 } else {
2480 eq_refs.inverted()
2481 };
2482 }
2483 _ => {}
2484 }
2485 }
2486 Refinements::empty()
2487 }
2488
2489 fn extract_typeof_refinements(
2491 op: &str,
2492 left: &SNode,
2493 right: &SNode,
2494 scope: &TypeScope,
2495 ) -> Refinements {
2496 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
2497 (extract_type_of_var(left), &right.node)
2498 {
2499 (var, tn.clone())
2500 } else if let (Node::StringLiteral(tn), Some(var)) =
2501 (&left.node, extract_type_of_var(right))
2502 {
2503 (var, tn.clone())
2504 } else {
2505 return Refinements::empty();
2506 };
2507
2508 const KNOWN_TYPES: &[&str] = &[
2509 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
2510 ];
2511 if !KNOWN_TYPES.contains(&type_name.as_str()) {
2512 return Refinements::empty();
2513 }
2514
2515 let var_type = scope.get_var(&var_name).cloned().flatten();
2516 match var_type {
2517 Some(TypeExpr::Union(ref members)) => {
2518 let narrowed = narrow_to_single(members, &type_name);
2519 let remaining = remove_from_union(members, &type_name);
2520 if narrowed.is_some() || remaining.is_some() {
2521 let eq_refs = Refinements {
2522 truthy: narrowed
2523 .map(|n| vec![(var_name.clone(), Some(n))])
2524 .unwrap_or_default(),
2525 falsy: remaining
2526 .map(|r| vec![(var_name.clone(), Some(r))])
2527 .unwrap_or_default(),
2528 };
2529 return if op == "==" {
2530 eq_refs
2531 } else {
2532 eq_refs.inverted()
2533 };
2534 }
2535 }
2536 Some(TypeExpr::Named(ref n)) if n == &type_name => {
2537 let eq_refs = Refinements {
2540 truthy: vec![(var_name.clone(), Some(TypeExpr::Named(type_name)))],
2541 falsy: vec![(var_name.clone(), Some(TypeExpr::Never))],
2542 };
2543 return if op == "==" {
2544 eq_refs
2545 } else {
2546 eq_refs.inverted()
2547 };
2548 }
2549 _ => {}
2550 }
2551 Refinements::empty()
2552 }
2553
2554 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
2556 if let Node::Identifier(var_name) = &object.node {
2557 if let Node::StringLiteral(key) = &args[0].node {
2558 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
2559 if fields.iter().any(|f| f.name == *key && f.optional) {
2560 let narrowed_fields: Vec<ShapeField> = fields
2561 .iter()
2562 .map(|f| {
2563 if f.name == *key {
2564 ShapeField {
2565 name: f.name.clone(),
2566 type_expr: f.type_expr.clone(),
2567 optional: false,
2568 }
2569 } else {
2570 f.clone()
2571 }
2572 })
2573 .collect();
2574 return Refinements {
2575 truthy: vec![(
2576 var_name.clone(),
2577 Some(TypeExpr::Shape(narrowed_fields)),
2578 )],
2579 falsy: vec![],
2580 };
2581 }
2582 }
2583 }
2584 }
2585 Refinements::empty()
2586 }
2587
2588 fn extract_schema_refinements(args: &[SNode], scope: &TypeScope) -> Refinements {
2589 let Node::Identifier(var_name) = &args[0].node else {
2590 return Refinements::empty();
2591 };
2592 let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) else {
2593 return Refinements::empty();
2594 };
2595 let Some(Some(var_type)) = scope.get_var(var_name).cloned() else {
2596 return Refinements::empty();
2597 };
2598
2599 let truthy = intersect_types(&var_type, &schema_type)
2600 .map(|ty| vec![(var_name.clone(), Some(ty))])
2601 .unwrap_or_default();
2602 let falsy = subtract_type(&var_type, &schema_type)
2603 .map(|ty| vec![(var_name.clone(), Some(ty))])
2604 .unwrap_or_default();
2605
2606 Refinements { truthy, falsy }
2607 }
2608
2609 fn block_definitely_exits(stmts: &[SNode]) -> bool {
2611 block_definitely_exits(stmts)
2612 }
2613
2614 fn check_match_exhaustiveness(
2615 &mut self,
2616 value: &SNode,
2617 arms: &[MatchArm],
2618 scope: &TypeScope,
2619 span: Span,
2620 ) {
2621 let enum_name = match &value.node {
2623 Node::PropertyAccess { object, property } if property == "variant" => {
2624 match self.infer_type(object, scope) {
2626 Some(TypeExpr::Named(name)) => {
2627 if scope.get_enum(&name).is_some() {
2628 Some(name)
2629 } else {
2630 None
2631 }
2632 }
2633 _ => None,
2634 }
2635 }
2636 _ => {
2637 match self.infer_type(value, scope) {
2639 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
2640 _ => None,
2641 }
2642 }
2643 };
2644
2645 let Some(enum_name) = enum_name else {
2646 self.check_match_exhaustiveness_union(value, arms, scope, span);
2648 return;
2649 };
2650 let Some(variants) = scope.get_enum(&enum_name) else {
2651 return;
2652 };
2653
2654 let mut covered: Vec<String> = Vec::new();
2656 let mut has_wildcard = false;
2657
2658 for arm in arms {
2659 match &arm.pattern.node {
2660 Node::StringLiteral(s) => covered.push(s.clone()),
2662 Node::Identifier(name)
2664 if name == "_"
2665 || !variants
2666 .variants
2667 .iter()
2668 .any(|variant| variant.name == *name) =>
2669 {
2670 has_wildcard = true;
2671 }
2672 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
2674 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
2676 _ => {
2677 has_wildcard = true;
2679 }
2680 }
2681 }
2682
2683 if has_wildcard {
2684 return;
2685 }
2686
2687 let missing: Vec<&String> = variants
2688 .variants
2689 .iter()
2690 .map(|variant| &variant.name)
2691 .filter(|variant| !covered.contains(variant))
2692 .collect();
2693 if !missing.is_empty() {
2694 let missing_str = missing
2695 .iter()
2696 .map(|s| format!("\"{}\"", s))
2697 .collect::<Vec<_>>()
2698 .join(", ");
2699 self.warning_at(
2700 format!(
2701 "Non-exhaustive match on enum {}: missing variants {}",
2702 enum_name, missing_str
2703 ),
2704 span,
2705 );
2706 }
2707 }
2708
2709 fn check_match_exhaustiveness_union(
2711 &mut self,
2712 value: &SNode,
2713 arms: &[MatchArm],
2714 scope: &TypeScope,
2715 span: Span,
2716 ) {
2717 let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
2718 return;
2719 };
2720 if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
2722 return;
2723 }
2724
2725 let mut has_wildcard = false;
2726 let mut covered_types: Vec<String> = Vec::new();
2727
2728 for arm in arms {
2729 match &arm.pattern.node {
2730 Node::NilLiteral => covered_types.push("nil".into()),
2733 Node::BoolLiteral(_) => {
2734 if !covered_types.contains(&"bool".into()) {
2735 covered_types.push("bool".into());
2736 }
2737 }
2738 Node::IntLiteral(_) => {
2739 if !covered_types.contains(&"int".into()) {
2740 covered_types.push("int".into());
2741 }
2742 }
2743 Node::FloatLiteral(_) => {
2744 if !covered_types.contains(&"float".into()) {
2745 covered_types.push("float".into());
2746 }
2747 }
2748 Node::StringLiteral(_) => {
2749 if !covered_types.contains(&"string".into()) {
2750 covered_types.push("string".into());
2751 }
2752 }
2753 Node::Identifier(name) if name == "_" => {
2754 has_wildcard = true;
2755 }
2756 _ => {
2757 has_wildcard = true;
2758 }
2759 }
2760 }
2761
2762 if has_wildcard {
2763 return;
2764 }
2765
2766 let type_names: Vec<&str> = members
2767 .iter()
2768 .filter_map(|m| match m {
2769 TypeExpr::Named(n) => Some(n.as_str()),
2770 _ => None,
2771 })
2772 .collect();
2773 let missing: Vec<&&str> = type_names
2774 .iter()
2775 .filter(|t| !covered_types.iter().any(|c| c == **t))
2776 .collect();
2777 if !missing.is_empty() {
2778 let missing_str = missing
2779 .iter()
2780 .map(|s| s.to_string())
2781 .collect::<Vec<_>>()
2782 .join(", ");
2783 self.warning_at(
2784 format!(
2785 "Non-exhaustive match on union type: missing {}",
2786 missing_str
2787 ),
2788 span,
2789 );
2790 }
2791 }
2792
2793 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
2794 if name == "unreachable" {
2797 if let Some(arg) = args.first() {
2798 if matches!(&arg.node, Node::Identifier(_)) {
2799 let arg_type = self.infer_type(arg, scope);
2800 if let Some(ref ty) = arg_type {
2801 if !matches!(ty, TypeExpr::Never) {
2802 self.error_at(
2803 format!(
2804 "unreachable() argument has type `{}` — not all cases are handled",
2805 format_type(ty)
2806 ),
2807 span,
2808 );
2809 }
2810 }
2811 }
2812 }
2813 for arg in args {
2814 self.check_node(arg, scope);
2815 }
2816 return;
2817 }
2818
2819 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
2821 if let Some(sig) = scope.get_fn(name).cloned() {
2822 if !has_spread
2823 && !is_builtin(name)
2824 && !sig.has_rest
2825 && (args.len() < sig.required_params || args.len() > sig.params.len())
2826 {
2827 let expected = if sig.required_params == sig.params.len() {
2828 format!("{}", sig.params.len())
2829 } else {
2830 format!("{}-{}", sig.required_params, sig.params.len())
2831 };
2832 self.warning_at(
2833 format!(
2834 "Function '{}' expects {} arguments, got {}",
2835 name,
2836 expected,
2837 args.len()
2838 ),
2839 span,
2840 );
2841 }
2842 let call_scope = if sig.type_param_names.is_empty() {
2845 scope.clone()
2846 } else {
2847 let mut s = scope.child();
2848 for tp_name in &sig.type_param_names {
2849 s.generic_type_params.insert(tp_name.clone());
2850 }
2851 s
2852 };
2853 let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
2854 let type_param_set: std::collections::BTreeSet<String> =
2855 sig.type_param_names.iter().cloned().collect();
2856 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2857 if let Some(param_ty) = param_type {
2858 if let Some(arg_ty) = self.infer_type(arg, scope) {
2859 if let Err(message) = Self::extract_type_bindings(
2860 param_ty,
2861 &arg_ty,
2862 &type_param_set,
2863 &mut type_bindings,
2864 ) {
2865 self.error_at(message, arg.span);
2866 }
2867 }
2868 }
2869 }
2870 for (i, (arg, (param_name, param_type))) in
2871 args.iter().zip(sig.params.iter()).enumerate()
2872 {
2873 if let Some(expected) = param_type {
2874 let actual = self.infer_type(arg, scope);
2875 if let Some(actual) = &actual {
2876 let expected = Self::apply_type_bindings(expected, &type_bindings);
2877 if !self.types_compatible(&expected, actual, &call_scope) {
2878 self.error_at(
2879 format!(
2880 "Argument {} ('{}'): expected {}, got {}",
2881 i + 1,
2882 param_name,
2883 format_type(&expected),
2884 format_type(actual)
2885 ),
2886 arg.span,
2887 );
2888 }
2889 }
2890 }
2891 }
2892 if !sig.where_clauses.is_empty() {
2893 for (type_param, bound) in &sig.where_clauses {
2894 if let Some(concrete_type) = type_bindings.get(type_param) {
2895 let concrete_name = format_type(concrete_type);
2896 let Some(base_type_name) = Self::base_type_name(concrete_type) else {
2897 self.error_at(
2898 format!(
2899 "Type '{}' does not satisfy interface '{}': only named types can satisfy interfaces (required by constraint `where {}: {}`)",
2900 concrete_name, bound, type_param, bound
2901 ),
2902 span,
2903 );
2904 continue;
2905 };
2906 if let Some(reason) = self.interface_mismatch_reason(
2907 base_type_name,
2908 bound,
2909 &BTreeMap::new(),
2910 scope,
2911 ) {
2912 self.error_at(
2913 format!(
2914 "Type '{}' does not satisfy interface '{}': {} \
2915 (required by constraint `where {}: {}`)",
2916 concrete_name, bound, reason, type_param, bound
2917 ),
2918 span,
2919 );
2920 }
2921 }
2922 }
2923 }
2924 }
2925 for arg in args {
2927 self.check_node(arg, scope);
2928 }
2929 }
2930
2931 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
2933 match &snode.node {
2934 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
2935 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
2936 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
2937 Some(TypeExpr::Named("string".into()))
2938 }
2939 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
2940 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
2941 Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
2942 Node::RangeExpr { .. } => Some(TypeExpr::Named("range".into())),
2946 Node::DictLiteral(entries) => {
2947 let mut fields = Vec::new();
2949 for entry in entries {
2950 let key = match &entry.key.node {
2951 Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
2952 _ => return Some(TypeExpr::Named("dict".into())),
2953 };
2954 let val_type = self
2955 .infer_type(&entry.value, scope)
2956 .unwrap_or(TypeExpr::Named("nil".into()));
2957 fields.push(ShapeField {
2958 name: key,
2959 type_expr: val_type,
2960 optional: false,
2961 });
2962 }
2963 if !fields.is_empty() {
2964 Some(TypeExpr::Shape(fields))
2965 } else {
2966 Some(TypeExpr::Named("dict".into()))
2967 }
2968 }
2969 Node::Closure { params, body, .. } => {
2970 let all_typed = params.iter().all(|p| p.type_expr.is_some());
2972 if all_typed && !params.is_empty() {
2973 let param_types: Vec<TypeExpr> =
2974 params.iter().filter_map(|p| p.type_expr.clone()).collect();
2975 let ret = body.last().and_then(|last| self.infer_type(last, scope));
2977 if let Some(ret_type) = ret {
2978 return Some(TypeExpr::FnType {
2979 params: param_types,
2980 return_type: Box::new(ret_type),
2981 });
2982 }
2983 }
2984 Some(TypeExpr::Named("closure".into()))
2985 }
2986
2987 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
2988
2989 Node::FunctionCall { name, args } => {
2990 if let Some(struct_info) = scope.get_struct(name) {
2992 return Some(Self::applied_type_or_name(
2993 name,
2994 struct_info
2995 .type_params
2996 .iter()
2997 .map(|_| Self::wildcard_type())
2998 .collect(),
2999 ));
3000 }
3001 if name == "Ok" {
3002 let ok_type = args
3003 .first()
3004 .and_then(|arg| self.infer_type(arg, scope))
3005 .unwrap_or_else(Self::wildcard_type);
3006 return Some(TypeExpr::Applied {
3007 name: "Result".into(),
3008 args: vec![ok_type, Self::wildcard_type()],
3009 });
3010 }
3011 if name == "Err" {
3012 let err_type = args
3013 .first()
3014 .and_then(|arg| self.infer_type(arg, scope))
3015 .unwrap_or_else(Self::wildcard_type);
3016 return Some(TypeExpr::Applied {
3017 name: "Result".into(),
3018 args: vec![Self::wildcard_type(), err_type],
3019 });
3020 }
3021 if let Some(sig) = scope.get_fn(name) {
3023 let mut return_type = sig.return_type.clone();
3024 if let Some(ty) = return_type.take() {
3025 if sig.type_param_names.is_empty() {
3026 return Some(ty);
3027 }
3028 let mut bindings = BTreeMap::new();
3029 let type_param_set: std::collections::BTreeSet<String> =
3030 sig.type_param_names.iter().cloned().collect();
3031 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
3032 if let Some(param_ty) = param_type {
3033 if let Some(arg_ty) = self.infer_type(arg, scope) {
3034 let _ = Self::extract_type_bindings(
3035 param_ty,
3036 &arg_ty,
3037 &type_param_set,
3038 &mut bindings,
3039 );
3040 }
3041 }
3042 }
3043 return Some(Self::apply_type_bindings(&ty, &bindings));
3044 }
3045 return None;
3046 }
3047 if name == "schema_expect" && args.len() >= 2 {
3049 if let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) {
3050 return Some(schema_type);
3051 }
3052 }
3053 if (name == "schema_check" || name == "schema_parse") && args.len() >= 2 {
3054 if let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) {
3055 return Some(TypeExpr::Applied {
3056 name: "Result".into(),
3057 args: vec![schema_type, TypeExpr::Named("string".into())],
3058 });
3059 }
3060 }
3061 if (name == "llm_call" || name == "llm_completion") && args.len() >= 3 {
3064 if let Some(schema_type) = Self::extract_llm_schema_from_options(args, scope) {
3065 return Some(TypeExpr::Shape(vec![
3066 ShapeField {
3067 name: "text".into(),
3068 type_expr: TypeExpr::Named("string".into()),
3069 optional: false,
3070 },
3071 ShapeField {
3072 name: "model".into(),
3073 type_expr: TypeExpr::Named("string".into()),
3074 optional: false,
3075 },
3076 ShapeField {
3077 name: "provider".into(),
3078 type_expr: TypeExpr::Named("string".into()),
3079 optional: false,
3080 },
3081 ShapeField {
3082 name: "input_tokens".into(),
3083 type_expr: TypeExpr::Named("int".into()),
3084 optional: false,
3085 },
3086 ShapeField {
3087 name: "output_tokens".into(),
3088 type_expr: TypeExpr::Named("int".into()),
3089 optional: false,
3090 },
3091 ShapeField {
3092 name: "data".into(),
3093 type_expr: schema_type,
3094 optional: false,
3095 },
3096 ShapeField {
3097 name: "visible_text".into(),
3098 type_expr: TypeExpr::Named("string".into()),
3099 optional: true,
3100 },
3101 ShapeField {
3102 name: "tool_calls".into(),
3103 type_expr: TypeExpr::Named("list".into()),
3104 optional: true,
3105 },
3106 ShapeField {
3107 name: "thinking".into(),
3108 type_expr: TypeExpr::Named("string".into()),
3109 optional: true,
3110 },
3111 ShapeField {
3112 name: "stop_reason".into(),
3113 type_expr: TypeExpr::Named("string".into()),
3114 optional: true,
3115 },
3116 ]));
3117 }
3118 }
3119 builtin_return_type(name)
3121 }
3122
3123 Node::BinaryOp { op, left, right } => {
3124 let lt = self.infer_type(left, scope);
3125 let rt = self.infer_type(right, scope);
3126 infer_binary_op_type(op, <, &rt)
3127 }
3128
3129 Node::UnaryOp { op, operand } => {
3130 let t = self.infer_type(operand, scope);
3131 match op.as_str() {
3132 "!" => Some(TypeExpr::Named("bool".into())),
3133 "-" => t, _ => None,
3135 }
3136 }
3137
3138 Node::Ternary {
3139 condition,
3140 true_expr,
3141 false_expr,
3142 } => {
3143 let refs = Self::extract_refinements(condition, scope);
3144
3145 let mut true_scope = scope.child();
3146 apply_refinements(&mut true_scope, &refs.truthy);
3147 let tt = self.infer_type(true_expr, &true_scope);
3148
3149 let mut false_scope = scope.child();
3150 apply_refinements(&mut false_scope, &refs.falsy);
3151 let ft = self.infer_type(false_expr, &false_scope);
3152
3153 match (&tt, &ft) {
3154 (Some(a), Some(b)) if a == b => tt,
3155 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
3156 (Some(_), None) => tt,
3157 (None, Some(_)) => ft,
3158 (None, None) => None,
3159 }
3160 }
3161
3162 Node::EnumConstruct {
3163 enum_name,
3164 variant,
3165 args,
3166 } => {
3167 if let Some(enum_info) = scope.get_enum(enum_name) {
3168 Some(self.infer_enum_type(enum_name, enum_info, variant, args, scope))
3169 } else {
3170 Some(TypeExpr::Named(enum_name.clone()))
3171 }
3172 }
3173
3174 Node::PropertyAccess { object, property } => {
3175 if let Node::Identifier(name) = &object.node {
3177 if let Some(enum_info) = scope.get_enum(name) {
3178 return Some(self.infer_enum_type(name, enum_info, property, &[], scope));
3179 }
3180 }
3181 if property == "variant" {
3183 let obj_type = self.infer_type(object, scope);
3184 if let Some(name) = obj_type.as_ref().and_then(Self::base_type_name) {
3185 if scope.get_enum(name).is_some() {
3186 return Some(TypeExpr::Named("string".into()));
3187 }
3188 }
3189 }
3190 let obj_type = self.infer_type(object, scope);
3192 if let Some(TypeExpr::Applied { name, args }) = &obj_type {
3194 if name == "Pair" && args.len() == 2 {
3195 if property == "first" {
3196 return Some(args[0].clone());
3197 } else if property == "second" {
3198 return Some(args[1].clone());
3199 }
3200 }
3201 }
3202 if let Some(TypeExpr::Shape(fields)) = &obj_type {
3203 if let Some(field) = fields.iter().find(|f| f.name == *property) {
3204 return Some(field.type_expr.clone());
3205 }
3206 }
3207 None
3208 }
3209
3210 Node::SubscriptAccess { object, index } => {
3211 let obj_type = self.infer_type(object, scope);
3212 match &obj_type {
3213 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
3214 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
3215 Some(TypeExpr::Shape(fields)) => {
3216 if let Node::StringLiteral(key) = &index.node {
3218 fields
3219 .iter()
3220 .find(|f| &f.name == key)
3221 .map(|f| f.type_expr.clone())
3222 } else {
3223 None
3224 }
3225 }
3226 Some(TypeExpr::Named(n)) if n == "list" => None,
3227 Some(TypeExpr::Named(n)) if n == "dict" => None,
3228 Some(TypeExpr::Named(n)) if n == "string" => {
3229 Some(TypeExpr::Named("string".into()))
3230 }
3231 _ => None,
3232 }
3233 }
3234 Node::SliceAccess { object, .. } => {
3235 let obj_type = self.infer_type(object, scope);
3237 match &obj_type {
3238 Some(TypeExpr::List(_)) => obj_type,
3239 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
3240 Some(TypeExpr::Named(n)) if n == "string" => {
3241 Some(TypeExpr::Named("string".into()))
3242 }
3243 _ => None,
3244 }
3245 }
3246 Node::MethodCall {
3247 object,
3248 method,
3249 args,
3250 }
3251 | Node::OptionalMethodCall {
3252 object,
3253 method,
3254 args,
3255 } => {
3256 if let Node::Identifier(name) = &object.node {
3257 if let Some(enum_info) = scope.get_enum(name) {
3258 return Some(self.infer_enum_type(name, enum_info, method, args, scope));
3259 }
3260 if name == "Result" && (method == "Ok" || method == "Err") {
3261 let ok_type = if method == "Ok" {
3262 args.first()
3263 .and_then(|arg| self.infer_type(arg, scope))
3264 .unwrap_or_else(Self::wildcard_type)
3265 } else {
3266 Self::wildcard_type()
3267 };
3268 let err_type = if method == "Err" {
3269 args.first()
3270 .and_then(|arg| self.infer_type(arg, scope))
3271 .unwrap_or_else(Self::wildcard_type)
3272 } else {
3273 Self::wildcard_type()
3274 };
3275 return Some(TypeExpr::Applied {
3276 name: "Result".into(),
3277 args: vec![ok_type, err_type],
3278 });
3279 }
3280 }
3281 let obj_type = self.infer_type(object, scope);
3282 let iter_elem_type: Option<TypeExpr> = match &obj_type {
3287 Some(TypeExpr::Iter(inner)) => Some((**inner).clone()),
3288 Some(TypeExpr::Named(n)) if n == "iter" => Some(TypeExpr::Named("any".into())),
3289 _ => None,
3290 };
3291 if let Some(t) = iter_elem_type {
3292 let pair = |k: TypeExpr, v: TypeExpr| TypeExpr::Applied {
3293 name: "Pair".into(),
3294 args: vec![k, v],
3295 };
3296 let iter_of = |ty: TypeExpr| TypeExpr::Iter(Box::new(ty));
3297 match method.as_str() {
3298 "iter" => return Some(iter_of(t)),
3299 "map" | "flat_map" => {
3300 return Some(TypeExpr::Named("iter".into()));
3304 }
3305 "filter" | "take" | "skip" | "take_while" | "skip_while" => {
3306 return Some(iter_of(t));
3307 }
3308 "zip" => {
3309 return Some(iter_of(pair(t, TypeExpr::Named("any".into()))));
3310 }
3311 "enumerate" => {
3312 return Some(iter_of(pair(TypeExpr::Named("int".into()), t)));
3313 }
3314 "chain" => return Some(iter_of(t)),
3315 "chunks" | "windows" => {
3316 return Some(iter_of(TypeExpr::List(Box::new(t))));
3317 }
3318 "to_list" => return Some(TypeExpr::List(Box::new(t))),
3320 "to_set" => {
3321 return Some(TypeExpr::Applied {
3322 name: "set".into(),
3323 args: vec![t],
3324 })
3325 }
3326 "to_dict" => return Some(TypeExpr::Named("dict".into())),
3327 "count" => return Some(TypeExpr::Named("int".into())),
3328 "sum" => {
3329 return Some(TypeExpr::Union(vec![
3330 TypeExpr::Named("int".into()),
3331 TypeExpr::Named("float".into()),
3332 ]))
3333 }
3334 "min" | "max" | "first" | "last" | "find" => {
3335 return Some(TypeExpr::Union(vec![t, TypeExpr::Named("nil".into())]));
3336 }
3337 "any" | "all" => return Some(TypeExpr::Named("bool".into())),
3338 "for_each" => return Some(TypeExpr::Named("nil".into())),
3339 "reduce" => return None,
3340 _ => {}
3341 }
3342 }
3343 if method == "iter" {
3348 match &obj_type {
3349 Some(TypeExpr::List(inner)) => {
3350 return Some(TypeExpr::Iter(Box::new((**inner).clone())));
3351 }
3352 Some(TypeExpr::DictType(k, v)) => {
3353 return Some(TypeExpr::Iter(Box::new(TypeExpr::Applied {
3354 name: "Pair".into(),
3355 args: vec![(**k).clone(), (**v).clone()],
3356 })));
3357 }
3358 Some(TypeExpr::Named(n))
3359 if n == "list" || n == "dict" || n == "set" || n == "string" =>
3360 {
3361 return Some(TypeExpr::Named("iter".into()));
3362 }
3363 _ => {}
3364 }
3365 }
3366 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
3367 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
3368 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
3369 match method.as_str() {
3370 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
3372 Some(TypeExpr::Named("bool".into()))
3373 }
3374 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
3376 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
3378 | "pad_left" | "pad_right" | "repeat" | "join" => {
3379 Some(TypeExpr::Named("string".into()))
3380 }
3381 "split" | "chars" => Some(TypeExpr::Named("list".into())),
3382 "filter" => {
3384 if is_dict {
3385 Some(TypeExpr::Named("dict".into()))
3386 } else {
3387 Some(TypeExpr::Named("list".into()))
3388 }
3389 }
3390 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
3392 "window" | "each_cons" | "sliding_window" => match &obj_type {
3393 Some(TypeExpr::List(inner)) => Some(TypeExpr::List(Box::new(
3394 TypeExpr::List(Box::new((**inner).clone())),
3395 ))),
3396 _ => Some(TypeExpr::Named("list".into())),
3397 },
3398 "reduce" | "find" | "first" | "last" => None,
3399 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
3401 "merge" | "map_values" | "rekey" | "map_keys" => {
3402 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
3406 Some(TypeExpr::DictType(
3407 Box::new(TypeExpr::Named("string".into())),
3408 v.clone(),
3409 ))
3410 } else {
3411 Some(TypeExpr::Named("dict".into()))
3412 }
3413 }
3414 "to_string" => Some(TypeExpr::Named("string".into())),
3416 "to_int" => Some(TypeExpr::Named("int".into())),
3417 "to_float" => Some(TypeExpr::Named("float".into())),
3418 _ => None,
3419 }
3420 }
3421
3422 Node::TryOperator { operand } => match self.infer_type(operand, scope) {
3424 Some(TypeExpr::Applied { name, args }) if name == "Result" && args.len() == 2 => {
3425 Some(args[0].clone())
3426 }
3427 Some(TypeExpr::Named(name)) if name == "Result" => None,
3428 _ => None,
3429 },
3430
3431 Node::ThrowStmt { .. }
3433 | Node::ReturnStmt { .. }
3434 | Node::BreakStmt
3435 | Node::ContinueStmt => Some(TypeExpr::Never),
3436
3437 Node::IfElse {
3439 then_body,
3440 else_body,
3441 ..
3442 } => {
3443 let then_type = self.infer_block_type(then_body, scope);
3444 let else_type = else_body
3445 .as_ref()
3446 .and_then(|eb| self.infer_block_type(eb, scope));
3447 match (then_type, else_type) {
3448 (Some(TypeExpr::Never), Some(TypeExpr::Never)) => Some(TypeExpr::Never),
3449 (Some(TypeExpr::Never), Some(other)) | (Some(other), Some(TypeExpr::Never)) => {
3450 Some(other)
3451 }
3452 (Some(t), Some(e)) if t == e => Some(t),
3453 (Some(t), Some(e)) => Some(simplify_union(vec![t, e])),
3454 (Some(t), None) => Some(t),
3455 (None, _) => None,
3456 }
3457 }
3458
3459 Node::TryExpr { body } => {
3460 let ok_type = self
3461 .infer_block_type(body, scope)
3462 .unwrap_or_else(Self::wildcard_type);
3463 let err_type = self
3464 .infer_try_error_type(body, scope)
3465 .unwrap_or_else(Self::wildcard_type);
3466 Some(TypeExpr::Applied {
3467 name: "Result".into(),
3468 args: vec![ok_type, err_type],
3469 })
3470 }
3471
3472 Node::StructConstruct {
3473 struct_name,
3474 fields,
3475 } => scope
3476 .get_struct(struct_name)
3477 .map(|struct_info| self.infer_struct_type(struct_name, struct_info, fields, scope))
3478 .or_else(|| Some(TypeExpr::Named(struct_name.clone()))),
3479
3480 _ => None,
3481 }
3482 }
3483
3484 fn infer_block_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
3486 if Self::block_definitely_exits(stmts) {
3487 return Some(TypeExpr::Never);
3488 }
3489 stmts.last().and_then(|s| self.infer_type(s, scope))
3490 }
3491
3492 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
3494 if Self::is_wildcard_type(expected) || Self::is_wildcard_type(actual) {
3495 return true;
3496 }
3497 if let TypeExpr::Named(name) = expected {
3499 if scope.is_generic_type_param(name) {
3500 return true;
3501 }
3502 }
3503 if let TypeExpr::Named(name) = actual {
3504 if scope.is_generic_type_param(name) {
3505 return true;
3506 }
3507 }
3508 let expected = self.resolve_alias(expected, scope);
3509 let actual = self.resolve_alias(actual, scope);
3510
3511 if let Some(iface_name) = Self::base_type_name(&expected) {
3513 if let Some(interface_info) = scope.get_interface(iface_name) {
3514 let mut interface_bindings = BTreeMap::new();
3515 if let TypeExpr::Applied { args, .. } = &expected {
3516 for (type_param, arg) in interface_info.type_params.iter().zip(args.iter()) {
3517 interface_bindings.insert(type_param.clone(), arg.clone());
3518 }
3519 }
3520 if let Some(type_name) = Self::base_type_name(&actual) {
3521 return self.satisfies_interface(
3522 type_name,
3523 iface_name,
3524 &interface_bindings,
3525 scope,
3526 );
3527 }
3528 return false;
3529 }
3530 }
3531
3532 match (&expected, &actual) {
3533 (_, TypeExpr::Never) => true,
3535 (TypeExpr::Never, _) => false,
3537 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
3538 (TypeExpr::Named(a), TypeExpr::Applied { name: b, .. })
3539 | (TypeExpr::Applied { name: a, .. }, TypeExpr::Named(b)) => a == b,
3540 (
3541 TypeExpr::Applied {
3542 name: expected_name,
3543 args: expected_args,
3544 },
3545 TypeExpr::Applied {
3546 name: actual_name,
3547 args: actual_args,
3548 },
3549 ) => {
3550 expected_name == actual_name
3551 && expected_args.len() == actual_args.len()
3552 && expected_args.iter().zip(actual_args.iter()).all(
3553 |(expected_arg, actual_arg)| {
3554 self.types_compatible(expected_arg, actual_arg, scope)
3555 },
3556 )
3557 }
3558 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
3561 act_members.iter().all(|am| {
3562 exp_members
3563 .iter()
3564 .any(|em| self.types_compatible(em, am, scope))
3565 })
3566 }
3567 (TypeExpr::Union(members), actual_type) => members
3568 .iter()
3569 .any(|m| self.types_compatible(m, actual_type, scope)),
3570 (expected_type, TypeExpr::Union(members)) => members
3571 .iter()
3572 .all(|m| self.types_compatible(expected_type, m, scope)),
3573 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
3574 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
3575 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
3576 if expected_field.optional {
3577 return true;
3578 }
3579 af.iter().any(|actual_field| {
3580 actual_field.name == expected_field.name
3581 && self.types_compatible(
3582 &expected_field.type_expr,
3583 &actual_field.type_expr,
3584 scope,
3585 )
3586 })
3587 }),
3588 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
3590 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
3591 keys_ok
3592 && af
3593 .iter()
3594 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
3595 }
3596 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
3598 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
3599 self.types_compatible(expected_inner, actual_inner, scope)
3600 }
3601 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
3602 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
3603 (TypeExpr::Iter(expected_inner), TypeExpr::Iter(actual_inner)) => {
3604 self.types_compatible(expected_inner, actual_inner, scope)
3605 }
3606 (TypeExpr::Named(n), TypeExpr::Iter(_)) if n == "iter" => true,
3607 (TypeExpr::Iter(_), TypeExpr::Named(n)) if n == "iter" => true,
3608 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
3609 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
3610 }
3611 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
3612 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
3613 (
3615 TypeExpr::FnType {
3616 params: ep,
3617 return_type: er,
3618 },
3619 TypeExpr::FnType {
3620 params: ap,
3621 return_type: ar,
3622 },
3623 ) => {
3624 ep.len() == ap.len()
3625 && ep
3626 .iter()
3627 .zip(ap.iter())
3628 .all(|(e, a)| self.types_compatible(e, a, scope))
3629 && self.types_compatible(er, ar, scope)
3630 }
3631 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
3633 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
3634 _ => false,
3635 }
3636 }
3637
3638 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
3639 match ty {
3640 TypeExpr::Named(name) => {
3641 if let Some(resolved) = scope.resolve_type(name) {
3642 return self.resolve_alias(resolved, scope);
3643 }
3644 ty.clone()
3645 }
3646 TypeExpr::Union(types) => TypeExpr::Union(
3647 types
3648 .iter()
3649 .map(|ty| self.resolve_alias(ty, scope))
3650 .collect(),
3651 ),
3652 TypeExpr::Shape(fields) => TypeExpr::Shape(
3653 fields
3654 .iter()
3655 .map(|field| ShapeField {
3656 name: field.name.clone(),
3657 type_expr: self.resolve_alias(&field.type_expr, scope),
3658 optional: field.optional,
3659 })
3660 .collect(),
3661 ),
3662 TypeExpr::List(inner) => TypeExpr::List(Box::new(self.resolve_alias(inner, scope))),
3663 TypeExpr::Iter(inner) => TypeExpr::Iter(Box::new(self.resolve_alias(inner, scope))),
3664 TypeExpr::DictType(key, value) => TypeExpr::DictType(
3665 Box::new(self.resolve_alias(key, scope)),
3666 Box::new(self.resolve_alias(value, scope)),
3667 ),
3668 TypeExpr::FnType {
3669 params,
3670 return_type,
3671 } => TypeExpr::FnType {
3672 params: params
3673 .iter()
3674 .map(|param| self.resolve_alias(param, scope))
3675 .collect(),
3676 return_type: Box::new(self.resolve_alias(return_type, scope)),
3677 },
3678 TypeExpr::Applied { name, args } => TypeExpr::Applied {
3679 name: name.clone(),
3680 args: args
3681 .iter()
3682 .map(|arg| self.resolve_alias(arg, scope))
3683 .collect(),
3684 },
3685 TypeExpr::Never => TypeExpr::Never,
3686 }
3687 }
3688
3689 fn error_at(&mut self, message: String, span: Span) {
3690 self.diagnostics.push(TypeDiagnostic {
3691 message,
3692 severity: DiagnosticSeverity::Error,
3693 span: Some(span),
3694 help: None,
3695 fix: None,
3696 });
3697 }
3698
3699 #[allow(dead_code)]
3700 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
3701 self.diagnostics.push(TypeDiagnostic {
3702 message,
3703 severity: DiagnosticSeverity::Error,
3704 span: Some(span),
3705 help: Some(help),
3706 fix: None,
3707 });
3708 }
3709
3710 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
3711 self.diagnostics.push(TypeDiagnostic {
3712 message,
3713 severity: DiagnosticSeverity::Error,
3714 span: Some(span),
3715 help: None,
3716 fix: Some(fix),
3717 });
3718 }
3719
3720 fn warning_at(&mut self, message: String, span: Span) {
3721 self.diagnostics.push(TypeDiagnostic {
3722 message,
3723 severity: DiagnosticSeverity::Warning,
3724 span: Some(span),
3725 help: None,
3726 fix: None,
3727 });
3728 }
3729
3730 #[allow(dead_code)]
3731 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
3732 self.diagnostics.push(TypeDiagnostic {
3733 message,
3734 severity: DiagnosticSeverity::Warning,
3735 span: Some(span),
3736 help: Some(help),
3737 fix: None,
3738 });
3739 }
3740
3741 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
3745 match &snode.node {
3746 Node::BinaryOp { op, left, right } => {
3747 self.check_binops(left, scope);
3748 self.check_binops(right, scope);
3749 let lt = self.infer_type(left, scope);
3750 let rt = self.infer_type(right, scope);
3751 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
3752 let span = snode.span;
3753 match op.as_str() {
3754 "+" => {
3755 let valid = matches!(
3756 (l.as_str(), r.as_str()),
3757 ("int" | "float", "int" | "float")
3758 | ("string", "string")
3759 | ("list", "list")
3760 | ("dict", "dict")
3761 );
3762 if !valid {
3763 let msg = format!("can't add {} and {}", l, r);
3764 let fix = if l == "string" || r == "string" {
3765 self.build_interpolation_fix(left, right, l == "string", span)
3766 } else {
3767 None
3768 };
3769 if let Some(fix) = fix {
3770 self.error_at_with_fix(msg, span, fix);
3771 } else {
3772 self.error_at(msg, span);
3773 }
3774 }
3775 }
3776 "-" | "/" | "%" | "**" => {
3777 let numeric = ["int", "float"];
3778 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
3779 self.error_at(
3780 format!(
3781 "can't use '{}' on {} and {} (needs numeric operands)",
3782 op, l, r
3783 ),
3784 span,
3785 );
3786 }
3787 }
3788 "*" => {
3789 let numeric = ["int", "float"];
3790 let is_numeric =
3791 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
3792 let is_string_repeat =
3793 (l == "string" && r == "int") || (l == "int" && r == "string");
3794 if !is_numeric && !is_string_repeat {
3795 self.error_at(
3796 format!("can't multiply {} and {} (try string * int)", l, r),
3797 span,
3798 );
3799 }
3800 }
3801 _ => {}
3802 }
3803 }
3804 }
3805 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
3807 _ => {}
3808 }
3809 }
3810
3811 fn build_interpolation_fix(
3813 &self,
3814 left: &SNode,
3815 right: &SNode,
3816 left_is_string: bool,
3817 expr_span: Span,
3818 ) -> Option<Vec<FixEdit>> {
3819 let src = self.source.as_ref()?;
3820 let (str_node, other_node) = if left_is_string {
3821 (left, right)
3822 } else {
3823 (right, left)
3824 };
3825 let str_text = src.get(str_node.span.start..str_node.span.end)?;
3826 let other_text = src.get(other_node.span.start..other_node.span.end)?;
3827 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
3829 if other_text.contains('}') || other_text.contains('"') {
3831 return None;
3832 }
3833 let replacement = if left_is_string {
3834 format!("\"{inner}${{{other_text}}}\"")
3835 } else {
3836 format!("\"${{{other_text}}}{inner}\"")
3837 };
3838 Some(vec![FixEdit {
3839 span: expr_span,
3840 replacement,
3841 }])
3842 }
3843}
3844
3845impl Default for TypeChecker {
3846 fn default() -> Self {
3847 Self::new()
3848 }
3849}
3850
3851fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
3853 match op {
3854 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
3855 Some(TypeExpr::Named("bool".into()))
3856 }
3857 "+" => match (left, right) {
3858 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3859 match (l.as_str(), r.as_str()) {
3860 ("int", "int") => Some(TypeExpr::Named("int".into())),
3861 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3862 ("string", "string") => Some(TypeExpr::Named("string".into())),
3863 ("list", "list") => Some(TypeExpr::Named("list".into())),
3864 ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
3865 _ => None,
3866 }
3867 }
3868 _ => None,
3869 },
3870 "-" | "/" | "%" => match (left, right) {
3871 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3872 match (l.as_str(), r.as_str()) {
3873 ("int", "int") => Some(TypeExpr::Named("int".into())),
3874 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3875 _ => None,
3876 }
3877 }
3878 _ => None,
3879 },
3880 "**" => match (left, right) {
3881 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3882 match (l.as_str(), r.as_str()) {
3883 ("int", "int") => Some(TypeExpr::Named("int".into())),
3884 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3885 _ => None,
3886 }
3887 }
3888 _ => None,
3889 },
3890 "*" => match (left, right) {
3891 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3892 match (l.as_str(), r.as_str()) {
3893 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
3894 ("int", "int") => Some(TypeExpr::Named("int".into())),
3895 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3896 _ => None,
3897 }
3898 }
3899 _ => None,
3900 },
3901 "??" => match (left, right) {
3902 (Some(TypeExpr::Union(members)), _) => {
3904 let non_nil: Vec<_> = members
3905 .iter()
3906 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
3907 .cloned()
3908 .collect();
3909 if non_nil.len() == 1 {
3910 Some(non_nil[0].clone())
3911 } else if non_nil.is_empty() {
3912 right.clone()
3913 } else {
3914 Some(TypeExpr::Union(non_nil))
3915 }
3916 }
3917 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
3919 (Some(l), _) => Some(l.clone()),
3921 (None, _) => right.clone(),
3923 },
3924 "|>" => None,
3925 _ => None,
3926 }
3927}
3928
3929pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
3934 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
3935 let mut details = Vec::new();
3936 for field in ef {
3937 if field.optional {
3938 continue;
3939 }
3940 match af.iter().find(|f| f.name == field.name) {
3941 None => details.push(format!(
3942 "missing field '{}' ({})",
3943 field.name,
3944 format_type(&field.type_expr)
3945 )),
3946 Some(actual_field) => {
3947 let e_str = format_type(&field.type_expr);
3948 let a_str = format_type(&actual_field.type_expr);
3949 if e_str != a_str {
3950 details.push(format!(
3951 "field '{}' has type {}, expected {}",
3952 field.name, a_str, e_str
3953 ));
3954 }
3955 }
3956 }
3957 }
3958 if details.is_empty() {
3959 None
3960 } else {
3961 Some(details.join("; "))
3962 }
3963 } else {
3964 None
3965 }
3966}
3967
3968fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
3971 matches!(
3972 &value.node,
3973 Node::IntLiteral(_)
3974 | Node::FloatLiteral(_)
3975 | Node::StringLiteral(_)
3976 | Node::BoolLiteral(_)
3977 | Node::NilLiteral
3978 | Node::ListLiteral(_)
3979 | Node::DictLiteral(_)
3980 | Node::InterpolatedString(_)
3981 )
3982}
3983
3984pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
3987 match &stmt.node {
3988 Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
3989 true
3990 }
3991 Node::IfElse {
3992 then_body,
3993 else_body: Some(else_body),
3994 ..
3995 } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
3996 _ => false,
3997 }
3998}
3999
4000pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
4002 stmts.iter().any(stmt_definitely_exits)
4003}
4004
4005pub fn format_type(ty: &TypeExpr) -> String {
4006 match ty {
4007 TypeExpr::Named(n) => n.clone(),
4008 TypeExpr::Union(types) => types
4009 .iter()
4010 .map(format_type)
4011 .collect::<Vec<_>>()
4012 .join(" | "),
4013 TypeExpr::Shape(fields) => {
4014 let inner: Vec<String> = fields
4015 .iter()
4016 .map(|f| {
4017 let opt = if f.optional { "?" } else { "" };
4018 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
4019 })
4020 .collect();
4021 format!("{{{}}}", inner.join(", "))
4022 }
4023 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
4024 TypeExpr::Iter(inner) => format!("iter<{}>", format_type(inner)),
4025 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
4026 TypeExpr::Applied { name, args } => {
4027 let args_str = args.iter().map(format_type).collect::<Vec<_>>().join(", ");
4028 format!("{name}<{args_str}>")
4029 }
4030 TypeExpr::FnType {
4031 params,
4032 return_type,
4033 } => {
4034 let params_str = params
4035 .iter()
4036 .map(format_type)
4037 .collect::<Vec<_>>()
4038 .join(", ");
4039 format!("fn({}) -> {}", params_str, format_type(return_type))
4040 }
4041 TypeExpr::Never => "never".to_string(),
4042 }
4043}
4044
4045fn simplify_union(members: Vec<TypeExpr>) -> TypeExpr {
4047 let filtered: Vec<TypeExpr> = members
4048 .into_iter()
4049 .filter(|m| !matches!(m, TypeExpr::Never))
4050 .collect();
4051 match filtered.len() {
4052 0 => TypeExpr::Never,
4053 1 => filtered.into_iter().next().unwrap(),
4054 _ => TypeExpr::Union(filtered),
4055 }
4056}
4057
4058fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
4061 let remaining: Vec<TypeExpr> = members
4062 .iter()
4063 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
4064 .cloned()
4065 .collect();
4066 match remaining.len() {
4067 0 => Some(TypeExpr::Never),
4068 1 => Some(remaining.into_iter().next().unwrap()),
4069 _ => Some(TypeExpr::Union(remaining)),
4070 }
4071}
4072
4073fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
4075 if members
4076 .iter()
4077 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
4078 {
4079 Some(TypeExpr::Named(target.to_string()))
4080 } else {
4081 None
4082 }
4083}
4084
4085fn extract_type_of_var(node: &SNode) -> Option<String> {
4087 if let Node::FunctionCall { name, args } = &node.node {
4088 if name == "type_of" && args.len() == 1 {
4089 if let Node::Identifier(var) = &args[0].node {
4090 return Some(var.clone());
4091 }
4092 }
4093 }
4094 None
4095}
4096
4097fn schema_type_expr_from_node(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
4098 match &node.node {
4099 Node::Identifier(name) => scope.get_schema_binding(name).cloned().flatten(),
4100 Node::DictLiteral(entries) => schema_type_expr_from_dict(entries, scope),
4101 _ => None,
4102 }
4103}
4104
4105fn schema_type_expr_from_dict(entries: &[DictEntry], scope: &TypeScope) -> Option<TypeExpr> {
4106 let mut type_name: Option<String> = None;
4107 let mut properties: Option<&SNode> = None;
4108 let mut required: Option<Vec<String>> = None;
4109 let mut items: Option<&SNode> = None;
4110 let mut union: Option<&SNode> = None;
4111 let mut nullable = false;
4112 let mut additional_properties: Option<&SNode> = None;
4113
4114 for entry in entries {
4115 let key = schema_entry_key(&entry.key)?;
4116 match key.as_str() {
4117 "type" => match &entry.value.node {
4118 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
4119 type_name = Some(normalize_schema_type_name(text));
4120 }
4121 Node::ListLiteral(items_list) => {
4122 let union_members = items_list
4123 .iter()
4124 .filter_map(|item| match &item.node {
4125 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
4126 Some(TypeExpr::Named(normalize_schema_type_name(text)))
4127 }
4128 _ => None,
4129 })
4130 .collect::<Vec<_>>();
4131 if !union_members.is_empty() {
4132 return Some(TypeExpr::Union(union_members));
4133 }
4134 }
4135 _ => {}
4136 },
4137 "properties" => properties = Some(&entry.value),
4138 "required" => {
4139 required = schema_required_names(&entry.value);
4140 }
4141 "items" => items = Some(&entry.value),
4142 "union" | "oneOf" | "anyOf" => union = Some(&entry.value),
4143 "nullable" => {
4144 nullable = matches!(entry.value.node, Node::BoolLiteral(true));
4145 }
4146 "additional_properties" | "additionalProperties" => {
4147 additional_properties = Some(&entry.value);
4148 }
4149 _ => {}
4150 }
4151 }
4152
4153 let mut schema_type = if let Some(union_node) = union {
4154 schema_union_type_expr(union_node, scope)?
4155 } else if let Some(properties_node) = properties {
4156 let property_entries = match &properties_node.node {
4157 Node::DictLiteral(entries) => entries,
4158 _ => return None,
4159 };
4160 let required_names = required.unwrap_or_default();
4161 let mut fields = Vec::new();
4162 for entry in property_entries {
4163 let field_name = schema_entry_key(&entry.key)?;
4164 let field_type = schema_type_expr_from_node(&entry.value, scope)?;
4165 fields.push(ShapeField {
4166 name: field_name.clone(),
4167 type_expr: field_type,
4168 optional: !required_names.contains(&field_name),
4169 });
4170 }
4171 TypeExpr::Shape(fields)
4172 } else if let Some(item_node) = items {
4173 TypeExpr::List(Box::new(schema_type_expr_from_node(item_node, scope)?))
4174 } else if let Some(type_name) = type_name {
4175 if type_name == "dict" {
4176 if let Some(extra_node) = additional_properties {
4177 let value_type = match &extra_node.node {
4178 Node::BoolLiteral(_) => None,
4179 _ => schema_type_expr_from_node(extra_node, scope),
4180 };
4181 if let Some(value_type) = value_type {
4182 TypeExpr::DictType(
4183 Box::new(TypeExpr::Named("string".into())),
4184 Box::new(value_type),
4185 )
4186 } else {
4187 TypeExpr::Named(type_name)
4188 }
4189 } else {
4190 TypeExpr::Named(type_name)
4191 }
4192 } else {
4193 TypeExpr::Named(type_name)
4194 }
4195 } else {
4196 return None;
4197 };
4198
4199 if nullable {
4200 schema_type = match schema_type {
4201 TypeExpr::Union(mut members) => {
4202 if !members
4203 .iter()
4204 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil"))
4205 {
4206 members.push(TypeExpr::Named("nil".into()));
4207 }
4208 TypeExpr::Union(members)
4209 }
4210 other => TypeExpr::Union(vec![other, TypeExpr::Named("nil".into())]),
4211 };
4212 }
4213
4214 Some(schema_type)
4215}
4216
4217fn schema_union_type_expr(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
4218 let Node::ListLiteral(items) = &node.node else {
4219 return None;
4220 };
4221 let members = items
4222 .iter()
4223 .filter_map(|item| schema_type_expr_from_node(item, scope))
4224 .collect::<Vec<_>>();
4225 match members.len() {
4226 0 => None,
4227 1 => members.into_iter().next(),
4228 _ => Some(TypeExpr::Union(members)),
4229 }
4230}
4231
4232fn schema_required_names(node: &SNode) -> Option<Vec<String>> {
4233 let Node::ListLiteral(items) = &node.node else {
4234 return None;
4235 };
4236 Some(
4237 items
4238 .iter()
4239 .filter_map(|item| match &item.node {
4240 Node::StringLiteral(text) | Node::RawStringLiteral(text) => Some(text.clone()),
4241 Node::Identifier(text) => Some(text.clone()),
4242 _ => None,
4243 })
4244 .collect(),
4245 )
4246}
4247
4248fn schema_entry_key(node: &SNode) -> Option<String> {
4249 match &node.node {
4250 Node::Identifier(name) => Some(name.clone()),
4251 Node::StringLiteral(name) | Node::RawStringLiteral(name) => Some(name.clone()),
4252 _ => None,
4253 }
4254}
4255
4256fn normalize_schema_type_name(text: &str) -> String {
4257 match text {
4258 "object" => "dict".into(),
4259 "array" => "list".into(),
4260 "integer" => "int".into(),
4261 "number" => "float".into(),
4262 "boolean" => "bool".into(),
4263 "null" => "nil".into(),
4264 other => other.into(),
4265 }
4266}
4267
4268fn intersect_types(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
4269 match (current, schema_type) {
4270 (TypeExpr::Union(members), other) => {
4271 let kept = members
4272 .iter()
4273 .filter_map(|member| intersect_types(member, other))
4274 .collect::<Vec<_>>();
4275 match kept.len() {
4276 0 => None,
4277 1 => kept.into_iter().next(),
4278 _ => Some(TypeExpr::Union(kept)),
4279 }
4280 }
4281 (other, TypeExpr::Union(members)) => {
4282 let kept = members
4283 .iter()
4284 .filter_map(|member| intersect_types(other, member))
4285 .collect::<Vec<_>>();
4286 match kept.len() {
4287 0 => None,
4288 1 => kept.into_iter().next(),
4289 _ => Some(TypeExpr::Union(kept)),
4290 }
4291 }
4292 (TypeExpr::Named(left), TypeExpr::Named(right)) if left == right => {
4293 Some(TypeExpr::Named(left.clone()))
4294 }
4295 (TypeExpr::Named(name), TypeExpr::Shape(fields)) if name == "dict" => {
4296 Some(TypeExpr::Shape(fields.clone()))
4297 }
4298 (TypeExpr::Shape(fields), TypeExpr::Named(name)) if name == "dict" => {
4299 Some(TypeExpr::Shape(fields.clone()))
4300 }
4301 (TypeExpr::Named(name), TypeExpr::List(inner)) if name == "list" => {
4302 Some(TypeExpr::List(inner.clone()))
4303 }
4304 (TypeExpr::List(inner), TypeExpr::Named(name)) if name == "list" => {
4305 Some(TypeExpr::List(inner.clone()))
4306 }
4307 (TypeExpr::Named(name), TypeExpr::DictType(key, value)) if name == "dict" => {
4308 Some(TypeExpr::DictType(key.clone(), value.clone()))
4309 }
4310 (TypeExpr::DictType(key, value), TypeExpr::Named(name)) if name == "dict" => {
4311 Some(TypeExpr::DictType(key.clone(), value.clone()))
4312 }
4313 (TypeExpr::Shape(_), TypeExpr::Shape(fields)) => Some(TypeExpr::Shape(fields.clone())),
4314 (TypeExpr::List(current_inner), TypeExpr::List(schema_inner)) => {
4315 intersect_types(current_inner, schema_inner)
4316 .map(|inner| TypeExpr::List(Box::new(inner)))
4317 }
4318 (
4319 TypeExpr::DictType(current_key, current_value),
4320 TypeExpr::DictType(schema_key, schema_value),
4321 ) => {
4322 let key = intersect_types(current_key, schema_key)?;
4323 let value = intersect_types(current_value, schema_value)?;
4324 Some(TypeExpr::DictType(Box::new(key), Box::new(value)))
4325 }
4326 _ => None,
4327 }
4328}
4329
4330fn subtract_type(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
4331 match current {
4332 TypeExpr::Union(members) => {
4333 let remaining = members
4334 .iter()
4335 .filter(|member| intersect_types(member, schema_type).is_none())
4336 .cloned()
4337 .collect::<Vec<_>>();
4338 match remaining.len() {
4339 0 => None,
4340 1 => remaining.into_iter().next(),
4341 _ => Some(TypeExpr::Union(remaining)),
4342 }
4343 }
4344 other if intersect_types(other, schema_type).is_some() => None,
4345 other => Some(other.clone()),
4346 }
4347}
4348
4349fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
4351 for (var_name, narrowed_type) in refinements {
4352 if !scope.narrowed_vars.contains_key(var_name) {
4354 if let Some(original) = scope.get_var(var_name).cloned() {
4355 scope.narrowed_vars.insert(var_name.clone(), original);
4356 }
4357 }
4358 scope.define_var(var_name, narrowed_type.clone());
4359 }
4360}
4361
4362#[cfg(test)]
4363mod tests {
4364 use super::*;
4365 use crate::Parser;
4366 use harn_lexer::Lexer;
4367
4368 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
4369 let mut lexer = Lexer::new(source);
4370 let tokens = lexer.tokenize().unwrap();
4371 let mut parser = Parser::new(tokens);
4372 let program = parser.parse().unwrap();
4373 TypeChecker::new().check(&program)
4374 }
4375
4376 fn errors(source: &str) -> Vec<String> {
4377 check_source(source)
4378 .into_iter()
4379 .filter(|d| d.severity == DiagnosticSeverity::Error)
4380 .map(|d| d.message)
4381 .collect()
4382 }
4383
4384 #[test]
4385 fn test_no_errors_for_untyped_code() {
4386 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
4387 assert!(errs.is_empty());
4388 }
4389
4390 #[test]
4391 fn test_correct_typed_let() {
4392 let errs = errors("pipeline t(task) { let x: int = 42 }");
4393 assert!(errs.is_empty());
4394 }
4395
4396 #[test]
4397 fn test_type_mismatch_let() {
4398 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
4399 assert_eq!(errs.len(), 1);
4400 assert!(errs[0].contains("declared as int"));
4401 assert!(errs[0].contains("assigned string"));
4402 }
4403
4404 #[test]
4405 fn test_correct_typed_fn() {
4406 let errs = errors(
4407 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
4408 );
4409 assert!(errs.is_empty());
4410 }
4411
4412 #[test]
4413 fn test_fn_arg_type_mismatch() {
4414 let errs = errors(
4415 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
4416add("hello", 2) }"#,
4417 );
4418 assert_eq!(errs.len(), 1);
4419 assert!(errs[0].contains("Argument 1"));
4420 assert!(errs[0].contains("expected int"));
4421 }
4422
4423 #[test]
4424 fn test_return_type_mismatch() {
4425 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
4426 assert_eq!(errs.len(), 1);
4427 assert!(errs[0].contains("return type doesn't match"));
4428 }
4429
4430 #[test]
4431 fn test_union_type_compatible() {
4432 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
4433 assert!(errs.is_empty());
4434 }
4435
4436 #[test]
4437 fn test_union_type_mismatch() {
4438 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
4439 assert_eq!(errs.len(), 1);
4440 assert!(errs[0].contains("declared as"));
4441 }
4442
4443 #[test]
4444 fn test_type_inference_propagation() {
4445 let errs = errors(
4446 r#"pipeline t(task) {
4447 fn add(a: int, b: int) -> int { return a + b }
4448 let result: string = add(1, 2)
4449}"#,
4450 );
4451 assert_eq!(errs.len(), 1);
4452 assert!(errs[0].contains("declared as"));
4453 assert!(errs[0].contains("string"));
4454 assert!(errs[0].contains("int"));
4455 }
4456
4457 #[test]
4458 fn test_generic_return_type_instantiates_from_callsite() {
4459 let errs = errors(
4460 r#"pipeline t(task) {
4461 fn identity<T>(x: T) -> T { return x }
4462 fn first<T>(items: list<T>) -> T { return items[0] }
4463 let n: int = identity(42)
4464 let s: string = first(["a", "b"])
4465}"#,
4466 );
4467 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4468 }
4469
4470 #[test]
4471 fn test_generic_type_param_must_bind_consistently() {
4472 let errs = errors(
4473 r#"pipeline t(task) {
4474 fn keep<T>(a: T, b: T) -> T { return a }
4475 keep(1, "x")
4476}"#,
4477 );
4478 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
4479 assert!(
4480 errs.iter()
4481 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
4482 "missing generic binding conflict error: {:?}",
4483 errs
4484 );
4485 assert!(
4486 errs.iter()
4487 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
4488 "missing instantiated argument mismatch error: {:?}",
4489 errs
4490 );
4491 }
4492
4493 #[test]
4494 fn test_generic_list_binding_propagates_element_type() {
4495 let errs = errors(
4496 r#"pipeline t(task) {
4497 fn first<T>(items: list<T>) -> T { return items[0] }
4498 let bad: string = first([1, 2, 3])
4499}"#,
4500 );
4501 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
4502 assert!(errs[0].contains("declared as string, but assigned int"));
4503 }
4504
4505 #[test]
4506 fn test_generic_struct_literal_instantiates_type_arguments() {
4507 let errs = errors(
4508 r#"pipeline t(task) {
4509 struct Pair<A, B> {
4510 first: A
4511 second: B
4512 }
4513 let pair: Pair<int, string> = Pair { first: 1, second: "two" }
4514}"#,
4515 );
4516 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4517 }
4518
4519 #[test]
4520 fn test_generic_enum_construct_instantiates_type_arguments() {
4521 let errs = errors(
4522 r#"pipeline t(task) {
4523 enum Option<T> {
4524 Some(value: T),
4525 None
4526 }
4527 let value: Option<int> = Option.Some(42)
4528}"#,
4529 );
4530 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4531 }
4532
4533 #[test]
4534 fn test_result_generic_type_compatibility() {
4535 let errs = errors(
4536 r#"pipeline t(task) {
4537 let ok: Result<int, string> = Result.Ok(42)
4538 let err: Result<int, string> = Result.Err("oops")
4539}"#,
4540 );
4541 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4542 }
4543
4544 #[test]
4545 fn test_result_generic_type_mismatch_reports_error() {
4546 let errs = errors(
4547 r#"pipeline t(task) {
4548 let bad: Result<int, string> = Result.Err(42)
4549}"#,
4550 );
4551 assert_eq!(errs.len(), 1, "expected 1 error, got: {errs:?}");
4552 assert!(errs[0].contains("Result<int, string>"));
4553 assert!(errs[0].contains("Result<_, int>"));
4554 }
4555
4556 #[test]
4557 fn test_builtin_return_type_inference() {
4558 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
4559 assert_eq!(errs.len(), 1);
4560 assert!(errs[0].contains("string"));
4561 assert!(errs[0].contains("int"));
4562 }
4563
4564 #[test]
4565 fn test_workflow_and_transcript_builtins_are_known() {
4566 let errs = errors(
4567 r#"pipeline t(task) {
4568 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
4569 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
4570 let run: dict = workflow_execute("task", flow, [], {})
4571 let tree: dict = load_run_tree("run.json")
4572 let fixture: dict = run_record_fixture(run?.run)
4573 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
4574 let diff: dict = run_record_diff(run?.run, run?.run)
4575 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
4576 let suite_report: dict = eval_suite_run(manifest)
4577 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
4578 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
4579 let selection: dict = artifact_editor_selection("src/main.rs", "main")
4580 let verify: dict = artifact_verification_result("verify", "ok")
4581 let test_result: dict = artifact_test_result("tests", "pass")
4582 let cmd: dict = artifact_command_result("cargo test", {status: 0})
4583 let patch: dict = artifact_diff("src/main.rs", "old", "new")
4584 let git: dict = artifact_git_diff("diff --git a b")
4585 let review: dict = artifact_diff_review(patch, "review me")
4586 let decision: dict = artifact_review_decision(review, "accepted")
4587 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
4588 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
4589 let apply: dict = artifact_apply_intent(review, "apply")
4590 let transcript = transcript_reset({metadata: {source: "test"}})
4591 let visible: string = transcript_render_visible(transcript_archive(transcript))
4592 let events: list = transcript_events(transcript)
4593 let context: string = artifact_context([], {max_artifacts: 1})
4594 println(report)
4595 println(run)
4596 println(tree)
4597 println(fixture)
4598 println(suite)
4599 println(diff)
4600 println(manifest)
4601 println(suite_report)
4602 println(wf)
4603 println(snap)
4604 println(selection)
4605 println(verify)
4606 println(test_result)
4607 println(cmd)
4608 println(patch)
4609 println(git)
4610 println(review)
4611 println(decision)
4612 println(proposal)
4613 println(bundle)
4614 println(apply)
4615 println(visible)
4616 println(events)
4617 println(context)
4618}"#,
4619 );
4620 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4621 }
4622
4623 #[test]
4624 fn test_binary_op_type_inference() {
4625 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
4626 assert_eq!(errs.len(), 1);
4627 }
4628
4629 #[test]
4630 fn test_exponentiation_requires_numeric_operands() {
4631 let errs = errors(r#"pipeline t(task) { let x = "nope" ** 2 }"#);
4632 assert!(
4633 errs.iter().any(|err| err.contains("can't use '**'")),
4634 "missing exponentiation type error: {errs:?}"
4635 );
4636 }
4637
4638 #[test]
4639 fn test_comparison_returns_bool() {
4640 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
4641 assert!(errs.is_empty());
4642 }
4643
4644 #[test]
4645 fn test_int_float_promotion() {
4646 let errs = errors("pipeline t(task) { let x: float = 42 }");
4647 assert!(errs.is_empty());
4648 }
4649
4650 #[test]
4651 fn test_untyped_code_no_errors() {
4652 let errs = errors(
4653 r#"pipeline t(task) {
4654 fn process(data) {
4655 let result = data + " processed"
4656 return result
4657 }
4658 log(process("hello"))
4659}"#,
4660 );
4661 assert!(errs.is_empty());
4662 }
4663
4664 #[test]
4665 fn test_type_alias() {
4666 let errs = errors(
4667 r#"pipeline t(task) {
4668 type Name = string
4669 let x: Name = "hello"
4670}"#,
4671 );
4672 assert!(errs.is_empty());
4673 }
4674
4675 #[test]
4676 fn test_type_alias_mismatch() {
4677 let errs = errors(
4678 r#"pipeline t(task) {
4679 type Name = string
4680 let x: Name = 42
4681}"#,
4682 );
4683 assert_eq!(errs.len(), 1);
4684 }
4685
4686 #[test]
4687 fn test_assignment_type_check() {
4688 let errs = errors(
4689 r#"pipeline t(task) {
4690 var x: int = 0
4691 x = "hello"
4692}"#,
4693 );
4694 assert_eq!(errs.len(), 1);
4695 assert!(errs[0].contains("can't assign string"));
4696 }
4697
4698 #[test]
4699 fn test_covariance_int_to_float_in_fn() {
4700 let errs = errors(
4701 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
4702 );
4703 assert!(errs.is_empty());
4704 }
4705
4706 #[test]
4707 fn test_covariance_return_type() {
4708 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
4709 assert!(errs.is_empty());
4710 }
4711
4712 #[test]
4713 fn test_no_contravariance_float_to_int() {
4714 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
4715 assert_eq!(errs.len(), 1);
4716 }
4717
4718 fn warnings(source: &str) -> Vec<String> {
4721 check_source(source)
4722 .into_iter()
4723 .filter(|d| d.severity == DiagnosticSeverity::Warning)
4724 .map(|d| d.message)
4725 .collect()
4726 }
4727
4728 #[test]
4729 fn test_exhaustive_match_no_warning() {
4730 let warns = warnings(
4731 r#"pipeline t(task) {
4732 enum Color { Red, Green, Blue }
4733 let c = Color.Red
4734 match c.variant {
4735 "Red" -> { log("r") }
4736 "Green" -> { log("g") }
4737 "Blue" -> { log("b") }
4738 }
4739}"#,
4740 );
4741 let exhaustive_warns: Vec<_> = warns
4742 .iter()
4743 .filter(|w| w.contains("Non-exhaustive"))
4744 .collect();
4745 assert!(exhaustive_warns.is_empty());
4746 }
4747
4748 #[test]
4749 fn test_non_exhaustive_match_warning() {
4750 let warns = warnings(
4751 r#"pipeline t(task) {
4752 enum Color { Red, Green, Blue }
4753 let c = Color.Red
4754 match c.variant {
4755 "Red" -> { log("r") }
4756 "Green" -> { log("g") }
4757 }
4758}"#,
4759 );
4760 let exhaustive_warns: Vec<_> = warns
4761 .iter()
4762 .filter(|w| w.contains("Non-exhaustive"))
4763 .collect();
4764 assert_eq!(exhaustive_warns.len(), 1);
4765 assert!(exhaustive_warns[0].contains("Blue"));
4766 }
4767
4768 #[test]
4769 fn test_non_exhaustive_multiple_missing() {
4770 let warns = warnings(
4771 r#"pipeline t(task) {
4772 enum Status { Active, Inactive, Pending }
4773 let s = Status.Active
4774 match s.variant {
4775 "Active" -> { log("a") }
4776 }
4777}"#,
4778 );
4779 let exhaustive_warns: Vec<_> = warns
4780 .iter()
4781 .filter(|w| w.contains("Non-exhaustive"))
4782 .collect();
4783 assert_eq!(exhaustive_warns.len(), 1);
4784 assert!(exhaustive_warns[0].contains("Inactive"));
4785 assert!(exhaustive_warns[0].contains("Pending"));
4786 }
4787
4788 #[test]
4789 fn test_enum_construct_type_inference() {
4790 let errs = errors(
4791 r#"pipeline t(task) {
4792 enum Color { Red, Green, Blue }
4793 let c: Color = Color.Red
4794}"#,
4795 );
4796 assert!(errs.is_empty());
4797 }
4798
4799 #[test]
4802 fn test_nil_coalescing_strips_nil() {
4803 let errs = errors(
4805 r#"pipeline t(task) {
4806 let x: string | nil = nil
4807 let y: string = x ?? "default"
4808}"#,
4809 );
4810 assert!(errs.is_empty());
4811 }
4812
4813 #[test]
4814 fn test_shape_mismatch_detail_missing_field() {
4815 let errs = errors(
4816 r#"pipeline t(task) {
4817 let x: {name: string, age: int} = {name: "hello"}
4818}"#,
4819 );
4820 assert_eq!(errs.len(), 1);
4821 assert!(
4822 errs[0].contains("missing field 'age'"),
4823 "expected detail about missing field, got: {}",
4824 errs[0]
4825 );
4826 }
4827
4828 #[test]
4829 fn test_shape_mismatch_detail_wrong_type() {
4830 let errs = errors(
4831 r#"pipeline t(task) {
4832 let x: {name: string, age: int} = {name: 42, age: 10}
4833}"#,
4834 );
4835 assert_eq!(errs.len(), 1);
4836 assert!(
4837 errs[0].contains("field 'name' has type int, expected string"),
4838 "expected detail about wrong type, got: {}",
4839 errs[0]
4840 );
4841 }
4842
4843 #[test]
4846 fn test_match_pattern_string_against_int() {
4847 let warns = warnings(
4848 r#"pipeline t(task) {
4849 let x: int = 42
4850 match x {
4851 "hello" -> { log("bad") }
4852 42 -> { log("ok") }
4853 }
4854}"#,
4855 );
4856 let pattern_warns: Vec<_> = warns
4857 .iter()
4858 .filter(|w| w.contains("Match pattern type mismatch"))
4859 .collect();
4860 assert_eq!(pattern_warns.len(), 1);
4861 assert!(pattern_warns[0].contains("matching int against string literal"));
4862 }
4863
4864 #[test]
4865 fn test_match_pattern_int_against_string() {
4866 let warns = warnings(
4867 r#"pipeline t(task) {
4868 let x: string = "hello"
4869 match x {
4870 42 -> { log("bad") }
4871 "hello" -> { log("ok") }
4872 }
4873}"#,
4874 );
4875 let pattern_warns: Vec<_> = warns
4876 .iter()
4877 .filter(|w| w.contains("Match pattern type mismatch"))
4878 .collect();
4879 assert_eq!(pattern_warns.len(), 1);
4880 assert!(pattern_warns[0].contains("matching string against int literal"));
4881 }
4882
4883 #[test]
4884 fn test_match_pattern_bool_against_int() {
4885 let warns = warnings(
4886 r#"pipeline t(task) {
4887 let x: int = 42
4888 match x {
4889 true -> { log("bad") }
4890 42 -> { log("ok") }
4891 }
4892}"#,
4893 );
4894 let pattern_warns: Vec<_> = warns
4895 .iter()
4896 .filter(|w| w.contains("Match pattern type mismatch"))
4897 .collect();
4898 assert_eq!(pattern_warns.len(), 1);
4899 assert!(pattern_warns[0].contains("matching int against bool literal"));
4900 }
4901
4902 #[test]
4903 fn test_match_pattern_float_against_string() {
4904 let warns = warnings(
4905 r#"pipeline t(task) {
4906 let x: string = "hello"
4907 match x {
4908 3.14 -> { log("bad") }
4909 "hello" -> { log("ok") }
4910 }
4911}"#,
4912 );
4913 let pattern_warns: Vec<_> = warns
4914 .iter()
4915 .filter(|w| w.contains("Match pattern type mismatch"))
4916 .collect();
4917 assert_eq!(pattern_warns.len(), 1);
4918 assert!(pattern_warns[0].contains("matching string against float literal"));
4919 }
4920
4921 #[test]
4922 fn test_match_pattern_int_against_float_ok() {
4923 let warns = warnings(
4925 r#"pipeline t(task) {
4926 let x: float = 3.14
4927 match x {
4928 42 -> { log("ok") }
4929 _ -> { log("default") }
4930 }
4931}"#,
4932 );
4933 let pattern_warns: Vec<_> = warns
4934 .iter()
4935 .filter(|w| w.contains("Match pattern type mismatch"))
4936 .collect();
4937 assert!(pattern_warns.is_empty());
4938 }
4939
4940 #[test]
4941 fn test_match_pattern_float_against_int_ok() {
4942 let warns = warnings(
4944 r#"pipeline t(task) {
4945 let x: int = 42
4946 match x {
4947 3.14 -> { log("close") }
4948 _ -> { log("default") }
4949 }
4950}"#,
4951 );
4952 let pattern_warns: Vec<_> = warns
4953 .iter()
4954 .filter(|w| w.contains("Match pattern type mismatch"))
4955 .collect();
4956 assert!(pattern_warns.is_empty());
4957 }
4958
4959 #[test]
4960 fn test_match_pattern_correct_types_no_warning() {
4961 let warns = warnings(
4962 r#"pipeline t(task) {
4963 let x: int = 42
4964 match x {
4965 1 -> { log("one") }
4966 2 -> { log("two") }
4967 _ -> { log("other") }
4968 }
4969}"#,
4970 );
4971 let pattern_warns: Vec<_> = warns
4972 .iter()
4973 .filter(|w| w.contains("Match pattern type mismatch"))
4974 .collect();
4975 assert!(pattern_warns.is_empty());
4976 }
4977
4978 #[test]
4979 fn test_match_pattern_wildcard_no_warning() {
4980 let warns = warnings(
4981 r#"pipeline t(task) {
4982 let x: int = 42
4983 match x {
4984 _ -> { log("catch all") }
4985 }
4986}"#,
4987 );
4988 let pattern_warns: Vec<_> = warns
4989 .iter()
4990 .filter(|w| w.contains("Match pattern type mismatch"))
4991 .collect();
4992 assert!(pattern_warns.is_empty());
4993 }
4994
4995 #[test]
4996 fn test_match_pattern_untyped_no_warning() {
4997 let warns = warnings(
4999 r#"pipeline t(task) {
5000 let x = some_unknown_fn()
5001 match x {
5002 "hello" -> { log("string") }
5003 42 -> { log("int") }
5004 }
5005}"#,
5006 );
5007 let pattern_warns: Vec<_> = warns
5008 .iter()
5009 .filter(|w| w.contains("Match pattern type mismatch"))
5010 .collect();
5011 assert!(pattern_warns.is_empty());
5012 }
5013
5014 fn iface_errors(source: &str) -> Vec<String> {
5017 errors(source)
5018 .into_iter()
5019 .filter(|message| message.contains("does not satisfy interface"))
5020 .collect()
5021 }
5022
5023 #[test]
5024 fn test_interface_constraint_return_type_mismatch() {
5025 let warns = iface_errors(
5026 r#"pipeline t(task) {
5027 interface Sizable {
5028 fn size(self) -> int
5029 }
5030 struct Box { width: int }
5031 impl Box {
5032 fn size(self) -> string { return "nope" }
5033 }
5034 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
5035 measure(Box({width: 3}))
5036}"#,
5037 );
5038 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
5039 assert!(
5040 warns[0].contains("method 'size' returns 'string', expected 'int'"),
5041 "unexpected message: {}",
5042 warns[0]
5043 );
5044 }
5045
5046 #[test]
5047 fn test_interface_constraint_param_type_mismatch() {
5048 let warns = iface_errors(
5049 r#"pipeline t(task) {
5050 interface Processor {
5051 fn process(self, x: int) -> string
5052 }
5053 struct MyProc { name: string }
5054 impl MyProc {
5055 fn process(self, x: string) -> string { return x }
5056 }
5057 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
5058 run_proc(MyProc({name: "a"}))
5059}"#,
5060 );
5061 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
5062 assert!(
5063 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
5064 "unexpected message: {}",
5065 warns[0]
5066 );
5067 }
5068
5069 #[test]
5070 fn test_interface_constraint_missing_method() {
5071 let warns = iface_errors(
5072 r#"pipeline t(task) {
5073 interface Sizable {
5074 fn size(self) -> int
5075 }
5076 struct Box { width: int }
5077 impl Box {
5078 fn area(self) -> int { return self.width }
5079 }
5080 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
5081 measure(Box({width: 3}))
5082}"#,
5083 );
5084 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
5085 assert!(
5086 warns[0].contains("missing method 'size'"),
5087 "unexpected message: {}",
5088 warns[0]
5089 );
5090 }
5091
5092 #[test]
5093 fn test_interface_constraint_param_count_mismatch() {
5094 let warns = iface_errors(
5095 r#"pipeline t(task) {
5096 interface Doubler {
5097 fn double(self, x: int) -> int
5098 }
5099 struct Bad { v: int }
5100 impl Bad {
5101 fn double(self) -> int { return self.v * 2 }
5102 }
5103 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
5104 run_double(Bad({v: 5}))
5105}"#,
5106 );
5107 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
5108 assert!(
5109 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
5110 "unexpected message: {}",
5111 warns[0]
5112 );
5113 }
5114
5115 #[test]
5116 fn test_interface_constraint_satisfied() {
5117 let warns = iface_errors(
5118 r#"pipeline t(task) {
5119 interface Sizable {
5120 fn size(self) -> int
5121 }
5122 struct Box { width: int, height: int }
5123 impl Box {
5124 fn size(self) -> int { return self.width * self.height }
5125 }
5126 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
5127 measure(Box({width: 3, height: 4}))
5128}"#,
5129 );
5130 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5131 }
5132
5133 #[test]
5134 fn test_interface_constraint_untyped_impl_compatible() {
5135 let warns = iface_errors(
5137 r#"pipeline t(task) {
5138 interface Sizable {
5139 fn size(self) -> int
5140 }
5141 struct Box { width: int }
5142 impl Box {
5143 fn size(self) { return self.width }
5144 }
5145 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
5146 measure(Box({width: 3}))
5147}"#,
5148 );
5149 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5150 }
5151
5152 #[test]
5153 fn test_interface_constraint_int_float_covariance() {
5154 let warns = iface_errors(
5156 r#"pipeline t(task) {
5157 interface Measurable {
5158 fn value(self) -> float
5159 }
5160 struct Gauge { v: int }
5161 impl Gauge {
5162 fn value(self) -> int { return self.v }
5163 }
5164 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
5165 read_val(Gauge({v: 42}))
5166}"#,
5167 );
5168 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5169 }
5170
5171 #[test]
5172 fn test_interface_associated_type_constraint_satisfied() {
5173 let warns = iface_errors(
5174 r#"pipeline t(task) {
5175 interface Collection {
5176 type Item
5177 fn get(self, index: int) -> Item
5178 }
5179 struct Names {}
5180 impl Names {
5181 fn get(self, index: int) -> string { return "ada" }
5182 }
5183 fn first<C>(collection: C) where C: Collection {
5184 log(collection.get(0))
5185 }
5186 first(Names {})
5187}"#,
5188 );
5189 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5190 }
5191
5192 #[test]
5193 fn test_interface_associated_type_default_mismatch() {
5194 let warns = iface_errors(
5195 r#"pipeline t(task) {
5196 interface IntCollection {
5197 type Item = int
5198 fn get(self, index: int) -> Item
5199 }
5200 struct Labels {}
5201 impl Labels {
5202 fn get(self, index: int) -> string { return "oops" }
5203 }
5204 fn first<C>(collection: C) where C: IntCollection {
5205 log(collection.get(0))
5206 }
5207 first(Labels {})
5208}"#,
5209 );
5210 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
5211 assert!(
5212 warns[0].contains("associated type 'Item' resolves to 'string', expected 'int'"),
5213 "unexpected message: {}",
5214 warns[0]
5215 );
5216 }
5217
5218 #[test]
5221 fn test_nil_narrowing_then_branch() {
5222 let errs = errors(
5224 r#"pipeline t(task) {
5225 fn greet(name: string | nil) {
5226 if name != nil {
5227 let s: string = name
5228 }
5229 }
5230}"#,
5231 );
5232 assert!(errs.is_empty(), "got: {:?}", errs);
5233 }
5234
5235 #[test]
5236 fn test_nil_narrowing_else_branch() {
5237 let errs = errors(
5239 r#"pipeline t(task) {
5240 fn check(x: string | nil) {
5241 if x != nil {
5242 let s: string = x
5243 } else {
5244 let n: nil = x
5245 }
5246 }
5247}"#,
5248 );
5249 assert!(errs.is_empty(), "got: {:?}", errs);
5250 }
5251
5252 #[test]
5253 fn test_nil_equality_narrows_both() {
5254 let errs = errors(
5256 r#"pipeline t(task) {
5257 fn check(x: string | nil) {
5258 if x == nil {
5259 let n: nil = x
5260 } else {
5261 let s: string = x
5262 }
5263 }
5264}"#,
5265 );
5266 assert!(errs.is_empty(), "got: {:?}", errs);
5267 }
5268
5269 #[test]
5270 fn test_truthiness_narrowing() {
5271 let errs = errors(
5273 r#"pipeline t(task) {
5274 fn check(x: string | nil) {
5275 if x {
5276 let s: string = x
5277 }
5278 }
5279}"#,
5280 );
5281 assert!(errs.is_empty(), "got: {:?}", errs);
5282 }
5283
5284 #[test]
5285 fn test_negation_narrowing() {
5286 let errs = errors(
5288 r#"pipeline t(task) {
5289 fn check(x: string | nil) {
5290 if !x {
5291 let n: nil = x
5292 } else {
5293 let s: string = x
5294 }
5295 }
5296}"#,
5297 );
5298 assert!(errs.is_empty(), "got: {:?}", errs);
5299 }
5300
5301 #[test]
5302 fn test_typeof_narrowing() {
5303 let errs = errors(
5305 r#"pipeline t(task) {
5306 fn check(x: string | int) {
5307 if type_of(x) == "string" {
5308 let s: string = x
5309 }
5310 }
5311}"#,
5312 );
5313 assert!(errs.is_empty(), "got: {:?}", errs);
5314 }
5315
5316 #[test]
5317 fn test_typeof_narrowing_else() {
5318 let errs = errors(
5320 r#"pipeline t(task) {
5321 fn check(x: string | int) {
5322 if type_of(x) == "string" {
5323 let s: string = x
5324 } else {
5325 let i: int = x
5326 }
5327 }
5328}"#,
5329 );
5330 assert!(errs.is_empty(), "got: {:?}", errs);
5331 }
5332
5333 #[test]
5334 fn test_typeof_neq_narrowing() {
5335 let errs = errors(
5337 r#"pipeline t(task) {
5338 fn check(x: string | int) {
5339 if type_of(x) != "string" {
5340 let i: int = x
5341 } else {
5342 let s: string = x
5343 }
5344 }
5345}"#,
5346 );
5347 assert!(errs.is_empty(), "got: {:?}", errs);
5348 }
5349
5350 #[test]
5351 fn test_and_combines_narrowing() {
5352 let errs = errors(
5354 r#"pipeline t(task) {
5355 fn check(x: string | int | nil) {
5356 if x != nil && type_of(x) == "string" {
5357 let s: string = x
5358 }
5359 }
5360}"#,
5361 );
5362 assert!(errs.is_empty(), "got: {:?}", errs);
5363 }
5364
5365 #[test]
5366 fn test_or_falsy_narrowing() {
5367 let errs = errors(
5369 r#"pipeline t(task) {
5370 fn check(x: string | nil, y: int | nil) {
5371 if x || y {
5372 // conservative: can't narrow
5373 } else {
5374 let xn: nil = x
5375 let yn: nil = y
5376 }
5377 }
5378}"#,
5379 );
5380 assert!(errs.is_empty(), "got: {:?}", errs);
5381 }
5382
5383 #[test]
5384 fn test_guard_narrows_outer_scope() {
5385 let errs = errors(
5386 r#"pipeline t(task) {
5387 fn check(x: string | nil) {
5388 guard x != nil else { return }
5389 let s: string = x
5390 }
5391}"#,
5392 );
5393 assert!(errs.is_empty(), "got: {:?}", errs);
5394 }
5395
5396 #[test]
5397 fn test_while_narrows_body() {
5398 let errs = errors(
5399 r#"pipeline t(task) {
5400 fn check(x: string | nil) {
5401 while x != nil {
5402 let s: string = x
5403 break
5404 }
5405 }
5406}"#,
5407 );
5408 assert!(errs.is_empty(), "got: {:?}", errs);
5409 }
5410
5411 #[test]
5412 fn test_early_return_narrows_after_if() {
5413 let errs = errors(
5415 r#"pipeline t(task) {
5416 fn check(x: string | nil) -> string {
5417 if x == nil {
5418 return "default"
5419 }
5420 let s: string = x
5421 return s
5422 }
5423}"#,
5424 );
5425 assert!(errs.is_empty(), "got: {:?}", errs);
5426 }
5427
5428 #[test]
5429 fn test_early_throw_narrows_after_if() {
5430 let errs = errors(
5431 r#"pipeline t(task) {
5432 fn check(x: string | nil) {
5433 if x == nil {
5434 throw "missing"
5435 }
5436 let s: string = x
5437 }
5438}"#,
5439 );
5440 assert!(errs.is_empty(), "got: {:?}", errs);
5441 }
5442
5443 #[test]
5444 fn test_no_narrowing_unknown_type() {
5445 let errs = errors(
5447 r#"pipeline t(task) {
5448 fn check(x) {
5449 if x != nil {
5450 let s: string = x
5451 }
5452 }
5453}"#,
5454 );
5455 assert!(errs.is_empty(), "got: {:?}", errs);
5458 }
5459
5460 #[test]
5461 fn test_reassignment_invalidates_narrowing() {
5462 let errs = errors(
5464 r#"pipeline t(task) {
5465 fn check(x: string | nil) {
5466 var y: string | nil = x
5467 if y != nil {
5468 let s: string = y
5469 y = nil
5470 let s2: string = y
5471 }
5472 }
5473}"#,
5474 );
5475 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
5477 assert!(
5478 errs[0].contains("declared as"),
5479 "expected type mismatch, got: {}",
5480 errs[0]
5481 );
5482 }
5483
5484 #[test]
5485 fn test_let_immutable_warning() {
5486 let all = check_source(
5487 r#"pipeline t(task) {
5488 let x = 42
5489 x = 43
5490}"#,
5491 );
5492 let warnings: Vec<_> = all
5493 .iter()
5494 .filter(|d| d.severity == DiagnosticSeverity::Warning)
5495 .collect();
5496 assert!(
5497 warnings.iter().any(|w| w.message.contains("immutable")),
5498 "expected immutability warning, got: {:?}",
5499 warnings
5500 );
5501 }
5502
5503 #[test]
5504 fn test_nested_narrowing() {
5505 let errs = errors(
5506 r#"pipeline t(task) {
5507 fn check(x: string | int | nil) {
5508 if x != nil {
5509 if type_of(x) == "int" {
5510 let i: int = x
5511 }
5512 }
5513 }
5514}"#,
5515 );
5516 assert!(errs.is_empty(), "got: {:?}", errs);
5517 }
5518
5519 #[test]
5520 fn test_match_narrows_arms() {
5521 let errs = errors(
5522 r#"pipeline t(task) {
5523 fn check(x: string | int) {
5524 match x {
5525 "hello" -> {
5526 let s: string = x
5527 }
5528 42 -> {
5529 let i: int = x
5530 }
5531 _ -> {}
5532 }
5533 }
5534}"#,
5535 );
5536 assert!(errs.is_empty(), "got: {:?}", errs);
5537 }
5538
5539 #[test]
5540 fn test_has_narrows_optional_field() {
5541 let errs = errors(
5542 r#"pipeline t(task) {
5543 fn check(x: {name?: string, age: int}) {
5544 if x.has("name") {
5545 let n: {name: string, age: int} = x
5546 }
5547 }
5548}"#,
5549 );
5550 assert!(errs.is_empty(), "got: {:?}", errs);
5551 }
5552
5553 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
5558 let mut lexer = Lexer::new(source);
5559 let tokens = lexer.tokenize().unwrap();
5560 let mut parser = Parser::new(tokens);
5561 let program = parser.parse().unwrap();
5562 TypeChecker::new().check_with_source(&program, source)
5563 }
5564
5565 #[test]
5566 fn test_fix_string_plus_int_literal() {
5567 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
5568 let diags = check_source_with_source(source);
5569 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5570 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5571 let fix = fixable[0].fix.as_ref().unwrap();
5572 assert_eq!(fix.len(), 1);
5573 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
5574 }
5575
5576 #[test]
5577 fn test_fix_int_plus_string_literal() {
5578 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
5579 let diags = check_source_with_source(source);
5580 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5581 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5582 let fix = fixable[0].fix.as_ref().unwrap();
5583 assert_eq!(fix[0].replacement, "\"${42}hello\"");
5584 }
5585
5586 #[test]
5587 fn test_fix_string_plus_variable() {
5588 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
5589 let diags = check_source_with_source(source);
5590 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5591 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5592 let fix = fixable[0].fix.as_ref().unwrap();
5593 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
5594 }
5595
5596 #[test]
5597 fn test_no_fix_int_plus_int() {
5598 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}";
5600 let diags = check_source_with_source(source);
5601 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5602 assert!(
5603 fixable.is_empty(),
5604 "no fix expected for numeric ops, got: {fixable:?}"
5605 );
5606 }
5607
5608 #[test]
5609 fn test_no_fix_without_source() {
5610 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
5611 let diags = check_source(source);
5612 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5613 assert!(
5614 fixable.is_empty(),
5615 "without source, no fix should be generated"
5616 );
5617 }
5618
5619 #[test]
5622 fn test_union_exhaustive_match_no_warning() {
5623 let warns = warnings(
5624 r#"pipeline t(task) {
5625 let x: string | int | nil = nil
5626 match x {
5627 "hello" -> { log("s") }
5628 42 -> { log("i") }
5629 nil -> { log("n") }
5630 }
5631}"#,
5632 );
5633 let union_warns: Vec<_> = warns
5634 .iter()
5635 .filter(|w| w.contains("Non-exhaustive match on union"))
5636 .collect();
5637 assert!(union_warns.is_empty());
5638 }
5639
5640 #[test]
5641 fn test_union_non_exhaustive_match_warning() {
5642 let warns = warnings(
5643 r#"pipeline t(task) {
5644 let x: string | int | nil = nil
5645 match x {
5646 "hello" -> { log("s") }
5647 42 -> { log("i") }
5648 }
5649}"#,
5650 );
5651 let union_warns: Vec<_> = warns
5652 .iter()
5653 .filter(|w| w.contains("Non-exhaustive match on union"))
5654 .collect();
5655 assert_eq!(union_warns.len(), 1);
5656 assert!(union_warns[0].contains("nil"));
5657 }
5658
5659 #[test]
5662 fn test_nil_coalesce_non_union_preserves_left_type() {
5663 let errs = errors(
5665 r#"pipeline t(task) {
5666 let x: int = 42
5667 let y: int = x ?? 0
5668}"#,
5669 );
5670 assert!(errs.is_empty());
5671 }
5672
5673 #[test]
5674 fn test_nil_coalesce_nil_returns_right_type() {
5675 let errs = errors(
5676 r#"pipeline t(task) {
5677 let x: string = nil ?? "fallback"
5678}"#,
5679 );
5680 assert!(errs.is_empty());
5681 }
5682
5683 #[test]
5686 fn test_never_is_subtype_of_everything() {
5687 let tc = TypeChecker::new();
5688 let scope = TypeScope::new();
5689 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &TypeExpr::Never, &scope));
5690 assert!(tc.types_compatible(&TypeExpr::Named("int".into()), &TypeExpr::Never, &scope));
5691 assert!(tc.types_compatible(
5692 &TypeExpr::Union(vec![
5693 TypeExpr::Named("string".into()),
5694 TypeExpr::Named("nil".into()),
5695 ]),
5696 &TypeExpr::Never,
5697 &scope,
5698 ));
5699 }
5700
5701 #[test]
5702 fn test_nothing_is_subtype_of_never() {
5703 let tc = TypeChecker::new();
5704 let scope = TypeScope::new();
5705 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("string".into()), &scope));
5706 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("int".into()), &scope));
5707 }
5708
5709 #[test]
5710 fn test_never_never_compatible() {
5711 let tc = TypeChecker::new();
5712 let scope = TypeScope::new();
5713 assert!(tc.types_compatible(&TypeExpr::Never, &TypeExpr::Never, &scope));
5714 }
5715
5716 #[test]
5717 fn test_simplify_union_removes_never() {
5718 assert_eq!(
5719 simplify_union(vec![TypeExpr::Never, TypeExpr::Named("string".into())]),
5720 TypeExpr::Named("string".into()),
5721 );
5722 assert_eq!(
5723 simplify_union(vec![TypeExpr::Never, TypeExpr::Never]),
5724 TypeExpr::Never,
5725 );
5726 assert_eq!(
5727 simplify_union(vec![
5728 TypeExpr::Named("string".into()),
5729 TypeExpr::Never,
5730 TypeExpr::Named("int".into()),
5731 ]),
5732 TypeExpr::Union(vec![
5733 TypeExpr::Named("string".into()),
5734 TypeExpr::Named("int".into()),
5735 ]),
5736 );
5737 }
5738
5739 #[test]
5740 fn test_remove_from_union_exhausted_returns_never() {
5741 let result = remove_from_union(&[TypeExpr::Named("string".into())], "string");
5742 assert_eq!(result, Some(TypeExpr::Never));
5743 }
5744
5745 #[test]
5746 fn test_if_else_one_branch_throws_infers_other() {
5747 let errs = errors(
5749 r#"pipeline t(task) {
5750 fn foo(x: bool) -> int {
5751 let result: int = if x { 42 } else { throw "err" }
5752 return result
5753 }
5754}"#,
5755 );
5756 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5757 }
5758
5759 #[test]
5760 fn test_if_else_both_branches_throw_infers_never() {
5761 let errs = errors(
5763 r#"pipeline t(task) {
5764 fn foo(x: bool) -> string {
5765 let result: string = if x { throw "a" } else { throw "b" }
5766 return result
5767 }
5768}"#,
5769 );
5770 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5771 }
5772
5773 #[test]
5776 fn test_unreachable_after_return() {
5777 let warns = warnings(
5778 r#"pipeline t(task) {
5779 fn foo() -> int {
5780 return 1
5781 let x = 2
5782 }
5783}"#,
5784 );
5785 assert!(
5786 warns.iter().any(|w| w.contains("unreachable")),
5787 "expected unreachable warning: {warns:?}"
5788 );
5789 }
5790
5791 #[test]
5792 fn test_unreachable_after_throw() {
5793 let warns = warnings(
5794 r#"pipeline t(task) {
5795 fn foo() {
5796 throw "err"
5797 let x = 2
5798 }
5799}"#,
5800 );
5801 assert!(
5802 warns.iter().any(|w| w.contains("unreachable")),
5803 "expected unreachable warning: {warns:?}"
5804 );
5805 }
5806
5807 #[test]
5808 fn test_unreachable_after_composite_exit() {
5809 let warns = warnings(
5810 r#"pipeline t(task) {
5811 fn foo(x: bool) {
5812 if x { return 1 } else { throw "err" }
5813 let y = 2
5814 }
5815}"#,
5816 );
5817 assert!(
5818 warns.iter().any(|w| w.contains("unreachable")),
5819 "expected unreachable warning: {warns:?}"
5820 );
5821 }
5822
5823 #[test]
5824 fn test_no_unreachable_warning_when_reachable() {
5825 let warns = warnings(
5826 r#"pipeline t(task) {
5827 fn foo(x: bool) {
5828 if x { return 1 }
5829 let y = 2
5830 }
5831}"#,
5832 );
5833 assert!(
5834 !warns.iter().any(|w| w.contains("unreachable")),
5835 "unexpected unreachable warning: {warns:?}"
5836 );
5837 }
5838
5839 #[test]
5842 fn test_catch_typed_error_variable() {
5843 let errs = errors(
5845 r#"pipeline t(task) {
5846 enum AppError { NotFound, Timeout }
5847 try {
5848 throw AppError.NotFound
5849 } catch (e: AppError) {
5850 let x: AppError = e
5851 }
5852}"#,
5853 );
5854 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5855 }
5856
5857 #[test]
5860 fn test_unreachable_with_never_arg_no_error() {
5861 let errs = errors(
5863 r#"pipeline t(task) {
5864 fn foo(x: string | int) {
5865 if type_of(x) == "string" { return }
5866 if type_of(x) == "int" { return }
5867 unreachable(x)
5868 }
5869}"#,
5870 );
5871 assert!(
5872 !errs.iter().any(|e| e.contains("unreachable")),
5873 "unexpected unreachable error: {errs:?}"
5874 );
5875 }
5876
5877 #[test]
5878 fn test_unreachable_with_remaining_types_errors() {
5879 let errs = errors(
5881 r#"pipeline t(task) {
5882 fn foo(x: string | int | nil) {
5883 if type_of(x) == "string" { return }
5884 unreachable(x)
5885 }
5886}"#,
5887 );
5888 assert!(
5889 errs.iter()
5890 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
5891 "expected unreachable error about remaining types: {errs:?}"
5892 );
5893 }
5894
5895 #[test]
5896 fn test_unreachable_no_args_no_compile_error() {
5897 let errs = errors(
5898 r#"pipeline t(task) {
5899 fn foo() {
5900 unreachable()
5901 }
5902}"#,
5903 );
5904 assert!(
5905 !errs
5906 .iter()
5907 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
5908 "unreachable() with no args should not produce type error: {errs:?}"
5909 );
5910 }
5911
5912 #[test]
5913 fn test_never_type_annotation_parses() {
5914 let errs = errors(
5915 r#"pipeline t(task) {
5916 fn foo() -> never {
5917 throw "always throws"
5918 }
5919}"#,
5920 );
5921 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5922 }
5923
5924 #[test]
5925 fn test_format_type_never() {
5926 assert_eq!(format_type(&TypeExpr::Never), "never");
5927 }
5928
5929 fn check_source_strict(source: &str) -> Vec<TypeDiagnostic> {
5932 let mut lexer = Lexer::new(source);
5933 let tokens = lexer.tokenize().unwrap();
5934 let mut parser = Parser::new(tokens);
5935 let program = parser.parse().unwrap();
5936 TypeChecker::with_strict_types(true).check(&program)
5937 }
5938
5939 fn strict_warnings(source: &str) -> Vec<String> {
5940 check_source_strict(source)
5941 .into_iter()
5942 .filter(|d| d.severity == DiagnosticSeverity::Warning)
5943 .map(|d| d.message)
5944 .collect()
5945 }
5946
5947 #[test]
5948 fn test_strict_types_json_parse_property_access() {
5949 let warns = strict_warnings(
5950 r#"pipeline t(task) {
5951 let data = json_parse("{}")
5952 log(data.name)
5953}"#,
5954 );
5955 assert!(
5956 warns.iter().any(|w| w.contains("unvalidated")),
5957 "expected unvalidated warning, got: {warns:?}"
5958 );
5959 }
5960
5961 #[test]
5962 fn test_strict_types_direct_chain_access() {
5963 let warns = strict_warnings(
5964 r#"pipeline t(task) {
5965 log(json_parse("{}").name)
5966}"#,
5967 );
5968 assert!(
5969 warns.iter().any(|w| w.contains("Direct property access")),
5970 "expected direct access warning, got: {warns:?}"
5971 );
5972 }
5973
5974 #[test]
5975 fn test_strict_types_schema_expect_clears() {
5976 let warns = strict_warnings(
5977 r#"pipeline t(task) {
5978 let my_schema = {type: "object", properties: {name: {type: "string"}}}
5979 let data = json_parse("{}")
5980 schema_expect(data, my_schema)
5981 log(data.name)
5982}"#,
5983 );
5984 assert!(
5985 !warns.iter().any(|w| w.contains("unvalidated")),
5986 "expected no unvalidated warning after schema_expect, got: {warns:?}"
5987 );
5988 }
5989
5990 #[test]
5991 fn test_strict_types_schema_is_if_guard() {
5992 let warns = strict_warnings(
5993 r#"pipeline t(task) {
5994 let my_schema = {type: "object", properties: {name: {type: "string"}}}
5995 let data = json_parse("{}")
5996 if schema_is(data, my_schema) {
5997 log(data.name)
5998 }
5999}"#,
6000 );
6001 assert!(
6002 !warns.iter().any(|w| w.contains("unvalidated")),
6003 "expected no unvalidated warning inside schema_is guard, got: {warns:?}"
6004 );
6005 }
6006
6007 #[test]
6008 fn test_strict_types_shape_annotation_clears() {
6009 let warns = strict_warnings(
6010 r#"pipeline t(task) {
6011 let data: {name: string, age: int} = json_parse("{}")
6012 log(data.name)
6013}"#,
6014 );
6015 assert!(
6016 !warns.iter().any(|w| w.contains("unvalidated")),
6017 "expected no warning with shape annotation, got: {warns:?}"
6018 );
6019 }
6020
6021 #[test]
6022 fn test_strict_types_propagation() {
6023 let warns = strict_warnings(
6024 r#"pipeline t(task) {
6025 let data = json_parse("{}")
6026 let x = data
6027 log(x.name)
6028}"#,
6029 );
6030 assert!(
6031 warns
6032 .iter()
6033 .any(|w| w.contains("unvalidated") && w.contains("'x'")),
6034 "expected propagation warning for x, got: {warns:?}"
6035 );
6036 }
6037
6038 #[test]
6039 fn test_strict_types_non_boundary_no_warning() {
6040 let warns = strict_warnings(
6041 r#"pipeline t(task) {
6042 let x = len("hello")
6043 log(x)
6044}"#,
6045 );
6046 assert!(
6047 !warns.iter().any(|w| w.contains("unvalidated")),
6048 "non-boundary function should not be flagged, got: {warns:?}"
6049 );
6050 }
6051
6052 #[test]
6053 fn test_strict_types_subscript_access() {
6054 let warns = strict_warnings(
6055 r#"pipeline t(task) {
6056 let data = json_parse("{}")
6057 log(data["name"])
6058}"#,
6059 );
6060 assert!(
6061 warns.iter().any(|w| w.contains("unvalidated")),
6062 "expected subscript warning, got: {warns:?}"
6063 );
6064 }
6065
6066 #[test]
6067 fn test_strict_types_disabled_by_default() {
6068 let diags = check_source(
6069 r#"pipeline t(task) {
6070 let data = json_parse("{}")
6071 log(data.name)
6072}"#,
6073 );
6074 assert!(
6075 !diags.iter().any(|d| d.message.contains("unvalidated")),
6076 "strict types should be off by default, got: {diags:?}"
6077 );
6078 }
6079
6080 #[test]
6081 fn test_strict_types_llm_call_without_schema() {
6082 let warns = strict_warnings(
6083 r#"pipeline t(task) {
6084 let result = llm_call("prompt", "system")
6085 log(result.text)
6086}"#,
6087 );
6088 assert!(
6089 warns.iter().any(|w| w.contains("unvalidated")),
6090 "llm_call without schema should warn, got: {warns:?}"
6091 );
6092 }
6093
6094 #[test]
6095 fn test_strict_types_llm_call_with_schema_clean() {
6096 let warns = strict_warnings(
6097 r#"pipeline t(task) {
6098 let result = llm_call("prompt", "system", {
6099 schema: {type: "object", properties: {name: {type: "string"}}}
6100 })
6101 log(result.data)
6102 log(result.text)
6103}"#,
6104 );
6105 assert!(
6106 !warns.iter().any(|w| w.contains("unvalidated")),
6107 "llm_call with schema should not warn, got: {warns:?}"
6108 );
6109 }
6110
6111 #[test]
6112 fn test_strict_types_schema_expect_result_typed() {
6113 let warns = strict_warnings(
6114 r#"pipeline t(task) {
6115 let my_schema = {type: "object", properties: {name: {type: "string"}}}
6116 let validated = schema_expect(json_parse("{}"), my_schema)
6117 log(validated.name)
6118}"#,
6119 );
6120 assert!(
6121 !warns.iter().any(|w| w.contains("unvalidated")),
6122 "schema_expect result should be typed, got: {warns:?}"
6123 );
6124 }
6125
6126 #[test]
6127 fn test_strict_types_realistic_orchestration() {
6128 let warns = strict_warnings(
6129 r#"pipeline t(task) {
6130 let payload_schema = {type: "object", properties: {
6131 name: {type: "string"},
6132 steps: {type: "list", items: {type: "string"}}
6133 }}
6134
6135 // Good: schema-aware llm_call
6136 let result = llm_call("generate a workflow", "system", {
6137 schema: payload_schema
6138 })
6139 let workflow_name = result.data.name
6140
6141 // Good: validate then access
6142 let raw = json_parse("{}")
6143 schema_expect(raw, payload_schema)
6144 let steps = raw.steps
6145
6146 log(workflow_name)
6147 log(steps)
6148}"#,
6149 );
6150 assert!(
6151 !warns.iter().any(|w| w.contains("unvalidated")),
6152 "validated orchestration should be clean, got: {warns:?}"
6153 );
6154 }
6155
6156 #[test]
6157 fn test_strict_types_llm_call_with_schema_via_variable() {
6158 let warns = strict_warnings(
6159 r#"pipeline t(task) {
6160 let my_schema = {type: "object", properties: {score: {type: "float"}}}
6161 let result = llm_call("rate this", "system", {
6162 schema: my_schema
6163 })
6164 log(result.data.score)
6165}"#,
6166 );
6167 assert!(
6168 !warns.iter().any(|w| w.contains("unvalidated")),
6169 "llm_call with schema variable should not warn, got: {warns:?}"
6170 );
6171 }
6172}