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