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