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 options,
1460 } => {
1461 self.check_node(expr, scope);
1462 for (key, value) in options {
1463 self.check_node(value, scope);
1468 if key == "max_concurrent" {
1469 if let Some(ty) = self.infer_type(value, scope) {
1470 if !matches!(ty, TypeExpr::Named(ref n) if n == "int") {
1471 self.error_at(
1472 format!(
1473 "`max_concurrent` on `parallel` must be int, got {ty:?}"
1474 ),
1475 value.span,
1476 );
1477 }
1478 }
1479 }
1480 }
1481 let mut par_scope = scope.child();
1482 if let Some(var) = variable {
1483 let var_type = match mode {
1484 ParallelMode::Count => Some(TypeExpr::Named("int".into())),
1485 ParallelMode::Each | ParallelMode::Settle => {
1486 match self.infer_type(expr, scope) {
1487 Some(TypeExpr::List(inner)) => Some(*inner),
1488 _ => None,
1489 }
1490 }
1491 };
1492 par_scope.define_var(var, var_type);
1493 }
1494 self.check_block(body, &mut par_scope);
1495 }
1496
1497 Node::SelectExpr {
1498 cases,
1499 timeout,
1500 default_body,
1501 } => {
1502 for case in cases {
1503 self.check_node(&case.channel, scope);
1504 let mut case_scope = scope.child();
1505 case_scope.define_var(&case.variable, None);
1506 self.check_block(&case.body, &mut case_scope);
1507 }
1508 if let Some((dur, body)) = timeout {
1509 self.check_node(dur, scope);
1510 let mut timeout_scope = scope.child();
1511 self.check_block(body, &mut timeout_scope);
1512 }
1513 if let Some(body) = default_body {
1514 let mut default_scope = scope.child();
1515 self.check_block(body, &mut default_scope);
1516 }
1517 }
1518
1519 Node::DeadlineBlock { duration, body } => {
1520 self.check_node(duration, scope);
1521 let mut block_scope = scope.child();
1522 self.check_block(body, &mut block_scope);
1523 }
1524
1525 Node::MutexBlock { body } | Node::DeferStmt { body } => {
1526 let mut block_scope = scope.child();
1527 self.check_block(body, &mut block_scope);
1528 }
1529
1530 Node::Retry { count, body } => {
1531 self.check_node(count, scope);
1532 let mut retry_scope = scope.child();
1533 self.check_block(body, &mut retry_scope);
1534 }
1535
1536 Node::Closure { params, body, .. } => {
1537 let mut closure_scope = scope.child();
1538 for p in params {
1539 closure_scope.define_var(&p.name, p.type_expr.clone());
1540 }
1541 self.check_block(body, &mut closure_scope);
1542 }
1543
1544 Node::ListLiteral(elements) => {
1545 for elem in elements {
1546 self.check_node(elem, scope);
1547 }
1548 }
1549
1550 Node::DictLiteral(entries) => {
1551 for entry in entries {
1552 self.check_node(&entry.key, scope);
1553 self.check_node(&entry.value, scope);
1554 }
1555 }
1556
1557 Node::RangeExpr { start, end, .. } => {
1558 self.check_node(start, scope);
1559 self.check_node(end, scope);
1560 }
1561
1562 Node::Spread(inner) => {
1563 self.check_node(inner, scope);
1564 }
1565
1566 Node::Block(stmts) => {
1567 let mut block_scope = scope.child();
1568 self.check_block(stmts, &mut block_scope);
1569 }
1570
1571 Node::YieldExpr { value } => {
1572 if let Some(v) = value {
1573 self.check_node(v, scope);
1574 }
1575 }
1576
1577 Node::StructConstruct {
1579 struct_name,
1580 fields,
1581 } => {
1582 for entry in fields {
1583 self.check_node(&entry.key, scope);
1584 self.check_node(&entry.value, scope);
1585 }
1586 if let Some(struct_info) = scope.get_struct(struct_name).cloned() {
1587 let type_bindings = self.infer_struct_bindings(&struct_info, fields, scope);
1588 for entry in fields {
1590 if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1591 if !struct_info.fields.iter().any(|field| field.name == *key) {
1592 self.warning_at(
1593 format!("Unknown field '{}' in struct '{}'", key, struct_name),
1594 entry.key.span,
1595 );
1596 }
1597 }
1598 }
1599 let provided: Vec<String> = fields
1601 .iter()
1602 .filter_map(|e| match &e.key.node {
1603 Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1604 _ => None,
1605 })
1606 .collect();
1607 for field in &struct_info.fields {
1608 if !field.optional && !provided.contains(&field.name) {
1609 self.warning_at(
1610 format!(
1611 "Missing field '{}' in struct '{}' construction",
1612 field.name, struct_name
1613 ),
1614 span,
1615 );
1616 }
1617 }
1618 for field in &struct_info.fields {
1619 let Some(expected_type) = &field.type_expr else {
1620 continue;
1621 };
1622 let Some(entry) = fields.iter().find(|entry| {
1623 matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
1624 }) else {
1625 continue;
1626 };
1627 let Some(actual_type) = self.infer_type(&entry.value, scope) else {
1628 continue;
1629 };
1630 let expected = Self::apply_type_bindings(expected_type, &type_bindings);
1631 if !self.types_compatible(&expected, &actual_type, scope) {
1632 self.error_at(
1633 format!(
1634 "Field '{}' in struct '{}' expects {}, got {}",
1635 field.name,
1636 struct_name,
1637 format_type(&expected),
1638 format_type(&actual_type)
1639 ),
1640 entry.value.span,
1641 );
1642 }
1643 }
1644 }
1645 }
1646
1647 Node::EnumConstruct {
1649 enum_name,
1650 variant,
1651 args,
1652 } => {
1653 for arg in args {
1654 self.check_node(arg, scope);
1655 }
1656 if let Some(enum_info) = scope.get_enum(enum_name).cloned() {
1657 let Some(enum_variant) = enum_info
1658 .variants
1659 .iter()
1660 .find(|enum_variant| enum_variant.name == *variant)
1661 else {
1662 self.warning_at(
1663 format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1664 span,
1665 );
1666 return;
1667 };
1668 if args.len() != enum_variant.fields.len() {
1669 self.warning_at(
1670 format!(
1671 "Variant '{}.{}' expects {} argument(s), got {}",
1672 enum_name,
1673 variant,
1674 enum_variant.fields.len(),
1675 args.len()
1676 ),
1677 span,
1678 );
1679 }
1680 let type_param_set: std::collections::BTreeSet<String> =
1681 enum_info.type_params.iter().cloned().collect();
1682 let mut type_bindings = BTreeMap::new();
1683 for (field, arg) in enum_variant.fields.iter().zip(args.iter()) {
1684 let Some(expected_type) = &field.type_expr else {
1685 continue;
1686 };
1687 let Some(actual_type) = self.infer_type(arg, scope) else {
1688 continue;
1689 };
1690 if let Err(message) = Self::extract_type_bindings(
1691 expected_type,
1692 &actual_type,
1693 &type_param_set,
1694 &mut type_bindings,
1695 ) {
1696 self.error_at(message, arg.span);
1697 }
1698 }
1699 for (index, (field, arg)) in
1700 enum_variant.fields.iter().zip(args.iter()).enumerate()
1701 {
1702 let Some(expected_type) = &field.type_expr else {
1703 continue;
1704 };
1705 let Some(actual_type) = self.infer_type(arg, scope) else {
1706 continue;
1707 };
1708 let expected = Self::apply_type_bindings(expected_type, &type_bindings);
1709 if !self.types_compatible(&expected, &actual_type, scope) {
1710 self.error_at(
1711 format!(
1712 "Variant '{}.{}' argument {} ('{}') expects {}, got {}",
1713 enum_name,
1714 variant,
1715 index + 1,
1716 field.name,
1717 format_type(&expected),
1718 format_type(&actual_type)
1719 ),
1720 arg.span,
1721 );
1722 }
1723 }
1724 }
1725 }
1726
1727 Node::InterpolatedString(_) => {}
1729
1730 Node::StringLiteral(_)
1732 | Node::RawStringLiteral(_)
1733 | Node::IntLiteral(_)
1734 | Node::FloatLiteral(_)
1735 | Node::BoolLiteral(_)
1736 | Node::NilLiteral
1737 | Node::Identifier(_)
1738 | Node::DurationLiteral(_)
1739 | Node::BreakStmt
1740 | Node::ContinueStmt
1741 | Node::ReturnStmt { value: None }
1742 | Node::ImportDecl { .. }
1743 | Node::SelectiveImport { .. } => {}
1744
1745 Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1748 let mut decl_scope = scope.child();
1749 self.check_block(body, &mut decl_scope);
1750 }
1751 }
1752 }
1753
1754 fn check_fn_body(
1755 &mut self,
1756 type_params: &[TypeParam],
1757 params: &[TypedParam],
1758 return_type: &Option<TypeExpr>,
1759 body: &[SNode],
1760 where_clauses: &[WhereClause],
1761 ) {
1762 let mut fn_scope = self.scope.child();
1763 for tp in type_params {
1766 fn_scope.generic_type_params.insert(tp.name.clone());
1767 }
1768 for wc in where_clauses {
1770 fn_scope
1771 .where_constraints
1772 .insert(wc.type_name.clone(), wc.bound.clone());
1773 }
1774 for param in params {
1775 fn_scope.define_var(¶m.name, param.type_expr.clone());
1776 if let Some(default) = ¶m.default_value {
1777 self.check_node(default, &mut fn_scope);
1778 }
1779 }
1780 let ret_scope_base = if return_type.is_some() {
1783 Some(fn_scope.child())
1784 } else {
1785 None
1786 };
1787
1788 self.check_block(body, &mut fn_scope);
1789
1790 if let Some(ret_type) = return_type {
1792 let mut ret_scope = ret_scope_base.unwrap();
1793 for stmt in body {
1794 self.check_return_type(stmt, ret_type, &mut ret_scope);
1795 }
1796 }
1797 }
1798
1799 fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1800 let span = snode.span;
1801 match &snode.node {
1802 Node::ReturnStmt { value: Some(val) } => {
1803 let inferred = self.infer_type(val, scope);
1804 if let Some(actual) = &inferred {
1805 if !self.types_compatible(expected, actual, scope) {
1806 self.error_at(
1807 format!(
1808 "Return type mismatch: expected {}, got {}",
1809 format_type(expected),
1810 format_type(actual)
1811 ),
1812 span,
1813 );
1814 }
1815 }
1816 }
1817 Node::IfElse {
1818 condition,
1819 then_body,
1820 else_body,
1821 } => {
1822 let refs = Self::extract_refinements(condition, scope);
1823 let mut then_scope = scope.child();
1824 apply_refinements(&mut then_scope, &refs.truthy);
1825 for stmt in then_body {
1826 self.check_return_type(stmt, expected, &mut then_scope);
1827 }
1828 if let Some(else_body) = else_body {
1829 let mut else_scope = scope.child();
1830 apply_refinements(&mut else_scope, &refs.falsy);
1831 for stmt in else_body {
1832 self.check_return_type(stmt, expected, &mut else_scope);
1833 }
1834 if Self::block_definitely_exits(then_body)
1836 && !Self::block_definitely_exits(else_body)
1837 {
1838 apply_refinements(scope, &refs.falsy);
1839 } else if Self::block_definitely_exits(else_body)
1840 && !Self::block_definitely_exits(then_body)
1841 {
1842 apply_refinements(scope, &refs.truthy);
1843 }
1844 } else {
1845 if Self::block_definitely_exits(then_body) {
1847 apply_refinements(scope, &refs.falsy);
1848 }
1849 }
1850 }
1851 _ => {}
1852 }
1853 }
1854
1855 fn satisfies_interface(
1861 &self,
1862 type_name: &str,
1863 interface_name: &str,
1864 interface_bindings: &BTreeMap<String, TypeExpr>,
1865 scope: &TypeScope,
1866 ) -> bool {
1867 self.interface_mismatch_reason(type_name, interface_name, interface_bindings, scope)
1868 .is_none()
1869 }
1870
1871 fn interface_mismatch_reason(
1874 &self,
1875 type_name: &str,
1876 interface_name: &str,
1877 interface_bindings: &BTreeMap<String, TypeExpr>,
1878 scope: &TypeScope,
1879 ) -> Option<String> {
1880 let interface_info = match scope.get_interface(interface_name) {
1881 Some(info) => info,
1882 None => return Some(format!("interface '{}' not found", interface_name)),
1883 };
1884 let impl_methods = match scope.get_impl_methods(type_name) {
1885 Some(methods) => methods,
1886 None => {
1887 if interface_info.methods.is_empty() {
1888 return None;
1889 }
1890 let names: Vec<_> = interface_info
1891 .methods
1892 .iter()
1893 .map(|m| m.name.as_str())
1894 .collect();
1895 return Some(format!("missing method(s): {}", names.join(", ")));
1896 }
1897 };
1898 let mut bindings = interface_bindings.clone();
1899 let associated_type_names: std::collections::BTreeSet<String> = interface_info
1900 .associated_types
1901 .iter()
1902 .map(|(name, _)| name.clone())
1903 .collect();
1904 for iface_method in &interface_info.methods {
1905 let iface_params: Vec<_> = iface_method
1906 .params
1907 .iter()
1908 .filter(|p| p.name != "self")
1909 .collect();
1910 let iface_param_count = iface_params.len();
1911 let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1912 let impl_method = match matching_impl {
1913 Some(m) => m,
1914 None => {
1915 return Some(format!("missing method '{}'", iface_method.name));
1916 }
1917 };
1918 if impl_method.param_count != iface_param_count {
1919 return Some(format!(
1920 "method '{}' has {} parameter(s), expected {}",
1921 iface_method.name, impl_method.param_count, iface_param_count
1922 ));
1923 }
1924 for (i, iface_param) in iface_params.iter().enumerate() {
1926 if let (Some(expected), Some(actual)) = (
1927 &iface_param.type_expr,
1928 impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1929 ) {
1930 if let Err(message) = Self::extract_type_bindings(
1931 expected,
1932 actual,
1933 &associated_type_names,
1934 &mut bindings,
1935 ) {
1936 return Some(message);
1937 }
1938 let expected = Self::apply_type_bindings(expected, &bindings);
1939 if !self.types_compatible(&expected, actual, scope) {
1940 return Some(format!(
1941 "method '{}' parameter {} has type '{}', expected '{}'",
1942 iface_method.name,
1943 i + 1,
1944 format_type(actual),
1945 format_type(&expected),
1946 ));
1947 }
1948 }
1949 }
1950 if let (Some(expected_ret), Some(actual_ret)) =
1952 (&iface_method.return_type, &impl_method.return_type)
1953 {
1954 if let Err(message) = Self::extract_type_bindings(
1955 expected_ret,
1956 actual_ret,
1957 &associated_type_names,
1958 &mut bindings,
1959 ) {
1960 return Some(message);
1961 }
1962 let expected_ret = Self::apply_type_bindings(expected_ret, &bindings);
1963 if !self.types_compatible(&expected_ret, actual_ret, scope) {
1964 return Some(format!(
1965 "method '{}' returns '{}', expected '{}'",
1966 iface_method.name,
1967 format_type(actual_ret),
1968 format_type(&expected_ret),
1969 ));
1970 }
1971 }
1972 }
1973 for (assoc_name, default_type) in &interface_info.associated_types {
1974 if let (Some(default_type), Some(actual)) = (default_type, bindings.get(assoc_name)) {
1975 let expected = Self::apply_type_bindings(default_type, &bindings);
1976 if !self.types_compatible(&expected, actual, scope) {
1977 return Some(format!(
1978 "associated type '{}' resolves to '{}', expected '{}'",
1979 assoc_name,
1980 format_type(actual),
1981 format_type(&expected),
1982 ));
1983 }
1984 }
1985 }
1986 None
1987 }
1988
1989 fn bind_type_param(
1990 param_name: &str,
1991 concrete: &TypeExpr,
1992 bindings: &mut BTreeMap<String, TypeExpr>,
1993 ) -> Result<(), String> {
1994 if let Some(existing) = bindings.get(param_name) {
1995 if existing != concrete {
1996 return Err(format!(
1997 "type parameter '{}' was inferred as both {} and {}",
1998 param_name,
1999 format_type(existing),
2000 format_type(concrete)
2001 ));
2002 }
2003 return Ok(());
2004 }
2005 bindings.insert(param_name.to_string(), concrete.clone());
2006 Ok(())
2007 }
2008
2009 fn extract_type_bindings(
2012 param_type: &TypeExpr,
2013 arg_type: &TypeExpr,
2014 type_params: &std::collections::BTreeSet<String>,
2015 bindings: &mut BTreeMap<String, TypeExpr>,
2016 ) -> Result<(), String> {
2017 match (param_type, arg_type) {
2018 (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
2019 Self::bind_type_param(param_name, concrete, bindings)
2020 }
2021 (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
2022 Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
2023 }
2024 (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
2025 Self::extract_type_bindings(pk, ak, type_params, bindings)?;
2026 Self::extract_type_bindings(pv, av, type_params, bindings)
2027 }
2028 (
2029 TypeExpr::Applied {
2030 name: p_name,
2031 args: p_args,
2032 },
2033 TypeExpr::Applied {
2034 name: a_name,
2035 args: a_args,
2036 },
2037 ) if p_name == a_name && p_args.len() == a_args.len() => {
2038 for (param, arg) in p_args.iter().zip(a_args.iter()) {
2039 Self::extract_type_bindings(param, arg, type_params, bindings)?;
2040 }
2041 Ok(())
2042 }
2043 (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
2044 for param_field in param_fields {
2045 if let Some(arg_field) = arg_fields
2046 .iter()
2047 .find(|field| field.name == param_field.name)
2048 {
2049 Self::extract_type_bindings(
2050 ¶m_field.type_expr,
2051 &arg_field.type_expr,
2052 type_params,
2053 bindings,
2054 )?;
2055 }
2056 }
2057 Ok(())
2058 }
2059 (
2060 TypeExpr::FnType {
2061 params: p_params,
2062 return_type: p_ret,
2063 },
2064 TypeExpr::FnType {
2065 params: a_params,
2066 return_type: a_ret,
2067 },
2068 ) => {
2069 for (param, arg) in p_params.iter().zip(a_params.iter()) {
2070 Self::extract_type_bindings(param, arg, type_params, bindings)?;
2071 }
2072 Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
2073 }
2074 _ => Ok(()),
2075 }
2076 }
2077
2078 fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
2079 match ty {
2080 TypeExpr::Named(name) => bindings
2081 .get(name)
2082 .cloned()
2083 .unwrap_or_else(|| TypeExpr::Named(name.clone())),
2084 TypeExpr::Union(items) => TypeExpr::Union(
2085 items
2086 .iter()
2087 .map(|item| Self::apply_type_bindings(item, bindings))
2088 .collect(),
2089 ),
2090 TypeExpr::Shape(fields) => TypeExpr::Shape(
2091 fields
2092 .iter()
2093 .map(|field| ShapeField {
2094 name: field.name.clone(),
2095 type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
2096 optional: field.optional,
2097 })
2098 .collect(),
2099 ),
2100 TypeExpr::List(inner) => {
2101 TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
2102 }
2103 TypeExpr::DictType(key, value) => TypeExpr::DictType(
2104 Box::new(Self::apply_type_bindings(key, bindings)),
2105 Box::new(Self::apply_type_bindings(value, bindings)),
2106 ),
2107 TypeExpr::Applied { name, args } => TypeExpr::Applied {
2108 name: name.clone(),
2109 args: args
2110 .iter()
2111 .map(|arg| Self::apply_type_bindings(arg, bindings))
2112 .collect(),
2113 },
2114 TypeExpr::FnType {
2115 params,
2116 return_type,
2117 } => TypeExpr::FnType {
2118 params: params
2119 .iter()
2120 .map(|param| Self::apply_type_bindings(param, bindings))
2121 .collect(),
2122 return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
2123 },
2124 TypeExpr::Never => TypeExpr::Never,
2125 }
2126 }
2127
2128 fn applied_type_or_name(name: &str, args: Vec<TypeExpr>) -> TypeExpr {
2129 if args.is_empty() {
2130 TypeExpr::Named(name.to_string())
2131 } else {
2132 TypeExpr::Applied {
2133 name: name.to_string(),
2134 args,
2135 }
2136 }
2137 }
2138
2139 fn infer_struct_bindings(
2140 &self,
2141 struct_info: &StructDeclInfo,
2142 fields: &[DictEntry],
2143 scope: &TypeScope,
2144 ) -> BTreeMap<String, TypeExpr> {
2145 let type_param_set: std::collections::BTreeSet<String> =
2146 struct_info.type_params.iter().cloned().collect();
2147 let mut bindings = BTreeMap::new();
2148 for field in &struct_info.fields {
2149 let Some(expected_type) = &field.type_expr else {
2150 continue;
2151 };
2152 let Some(entry) = fields.iter().find(|entry| {
2153 matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
2154 }) else {
2155 continue;
2156 };
2157 let Some(actual_type) = self.infer_type(&entry.value, scope) else {
2158 continue;
2159 };
2160 let _ = Self::extract_type_bindings(
2161 expected_type,
2162 &actual_type,
2163 &type_param_set,
2164 &mut bindings,
2165 );
2166 }
2167 bindings
2168 }
2169
2170 fn infer_struct_type(
2171 &self,
2172 struct_name: &str,
2173 struct_info: &StructDeclInfo,
2174 fields: &[DictEntry],
2175 scope: &TypeScope,
2176 ) -> TypeExpr {
2177 let bindings = self.infer_struct_bindings(struct_info, fields, scope);
2178 let args = struct_info
2179 .type_params
2180 .iter()
2181 .map(|name| {
2182 bindings
2183 .get(name)
2184 .cloned()
2185 .unwrap_or_else(Self::wildcard_type)
2186 })
2187 .collect();
2188 Self::applied_type_or_name(struct_name, args)
2189 }
2190
2191 fn infer_enum_type(
2192 &self,
2193 enum_name: &str,
2194 enum_info: &EnumDeclInfo,
2195 variant_name: &str,
2196 args: &[SNode],
2197 scope: &TypeScope,
2198 ) -> TypeExpr {
2199 let type_param_set: std::collections::BTreeSet<String> =
2200 enum_info.type_params.iter().cloned().collect();
2201 let mut bindings = BTreeMap::new();
2202 if let Some(variant) = enum_info
2203 .variants
2204 .iter()
2205 .find(|variant| variant.name == variant_name)
2206 {
2207 for (field, arg) in variant.fields.iter().zip(args.iter()) {
2208 let Some(expected_type) = &field.type_expr else {
2209 continue;
2210 };
2211 let Some(actual_type) = self.infer_type(arg, scope) else {
2212 continue;
2213 };
2214 let _ = Self::extract_type_bindings(
2215 expected_type,
2216 &actual_type,
2217 &type_param_set,
2218 &mut bindings,
2219 );
2220 }
2221 }
2222 let args = enum_info
2223 .type_params
2224 .iter()
2225 .map(|name| {
2226 bindings
2227 .get(name)
2228 .cloned()
2229 .unwrap_or_else(Self::wildcard_type)
2230 })
2231 .collect();
2232 Self::applied_type_or_name(enum_name, args)
2233 }
2234
2235 fn infer_try_error_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
2236 let mut inferred: Vec<TypeExpr> = Vec::new();
2237 for stmt in stmts {
2238 match &stmt.node {
2239 Node::ThrowStmt { value } => {
2240 if let Some(ty) = self.infer_type(value, scope) {
2241 inferred.push(ty);
2242 }
2243 }
2244 Node::TryOperator { operand } => {
2245 if let Some(TypeExpr::Applied { name, args }) = self.infer_type(operand, scope)
2246 {
2247 if name == "Result" && args.len() == 2 {
2248 inferred.push(args[1].clone());
2249 }
2250 }
2251 }
2252 Node::IfElse {
2253 then_body,
2254 else_body,
2255 ..
2256 } => {
2257 if let Some(ty) = self.infer_try_error_type(then_body, scope) {
2258 inferred.push(ty);
2259 }
2260 if let Some(else_body) = else_body {
2261 if let Some(ty) = self.infer_try_error_type(else_body, scope) {
2262 inferred.push(ty);
2263 }
2264 }
2265 }
2266 Node::Block(body)
2267 | Node::TryExpr { body }
2268 | Node::SpawnExpr { body }
2269 | Node::Retry { body, .. }
2270 | Node::WhileLoop { body, .. }
2271 | Node::DeferStmt { body }
2272 | Node::MutexBlock { body }
2273 | Node::DeadlineBlock { body, .. }
2274 | Node::Pipeline { body, .. }
2275 | Node::OverrideDecl { body, .. } => {
2276 if let Some(ty) = self.infer_try_error_type(body, scope) {
2277 inferred.push(ty);
2278 }
2279 }
2280 _ => {}
2281 }
2282 }
2283 if inferred.is_empty() {
2284 None
2285 } else {
2286 Some(simplify_union(inferred))
2287 }
2288 }
2289
2290 fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
2291 let mut inferred: Option<TypeExpr> = None;
2292 for item in items {
2293 let Some(item_type) = self.infer_type(item, scope) else {
2294 return TypeExpr::Named("list".into());
2295 };
2296 inferred = Some(match inferred {
2297 None => item_type,
2298 Some(current) if current == item_type => current,
2299 Some(TypeExpr::Union(mut members)) => {
2300 if !members.contains(&item_type) {
2301 members.push(item_type);
2302 }
2303 TypeExpr::Union(members)
2304 }
2305 Some(current) => TypeExpr::Union(vec![current, item_type]),
2306 });
2307 }
2308 inferred
2309 .map(|item_type| TypeExpr::List(Box::new(item_type)))
2310 .unwrap_or_else(|| TypeExpr::Named("list".into()))
2311 }
2312
2313 fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
2315 match &condition.node {
2316 Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
2318 let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
2319 if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
2320 return nil_ref;
2321 }
2322 let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
2323 if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
2324 return typeof_ref;
2325 }
2326 Refinements::empty()
2327 }
2328
2329 Node::BinaryOp { op, left, right } if op == "&&" => {
2331 let left_ref = Self::extract_refinements(left, scope);
2332 let right_ref = Self::extract_refinements(right, scope);
2333 let mut truthy = left_ref.truthy;
2334 truthy.extend(right_ref.truthy);
2335 Refinements {
2336 truthy,
2337 falsy: vec![],
2338 }
2339 }
2340
2341 Node::BinaryOp { op, left, right } if op == "||" => {
2343 let left_ref = Self::extract_refinements(left, scope);
2344 let right_ref = Self::extract_refinements(right, scope);
2345 let mut falsy = left_ref.falsy;
2346 falsy.extend(right_ref.falsy);
2347 Refinements {
2348 truthy: vec![],
2349 falsy,
2350 }
2351 }
2352
2353 Node::UnaryOp { op, operand } if op == "!" => {
2355 Self::extract_refinements(operand, scope).inverted()
2356 }
2357
2358 Node::Identifier(name) => {
2360 if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
2361 if members
2362 .iter()
2363 .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
2364 {
2365 if let Some(narrowed) = remove_from_union(members, "nil") {
2366 return Refinements {
2367 truthy: vec![(name.clone(), Some(narrowed))],
2368 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2369 };
2370 }
2371 }
2372 }
2373 Refinements::empty()
2374 }
2375
2376 Node::MethodCall {
2378 object,
2379 method,
2380 args,
2381 } if method == "has" && args.len() == 1 => {
2382 Self::extract_has_refinements(object, args, scope)
2383 }
2384
2385 Node::FunctionCall { name, args }
2386 if (name == "schema_is" || name == "is_type") && args.len() == 2 =>
2387 {
2388 Self::extract_schema_refinements(args, scope)
2389 }
2390
2391 _ => Refinements::empty(),
2392 }
2393 }
2394
2395 fn extract_nil_refinements(
2397 op: &str,
2398 left: &SNode,
2399 right: &SNode,
2400 scope: &TypeScope,
2401 ) -> Refinements {
2402 let var_node = if matches!(right.node, Node::NilLiteral) {
2403 left
2404 } else if matches!(left.node, Node::NilLiteral) {
2405 right
2406 } else {
2407 return Refinements::empty();
2408 };
2409
2410 if let Node::Identifier(name) = &var_node.node {
2411 let var_type = scope.get_var(name).cloned().flatten();
2412 match var_type {
2413 Some(TypeExpr::Union(ref members)) => {
2414 if let Some(narrowed) = remove_from_union(members, "nil") {
2415 let neq_refs = Refinements {
2416 truthy: vec![(name.clone(), Some(narrowed))],
2417 falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2418 };
2419 return if op == "!=" {
2420 neq_refs
2421 } else {
2422 neq_refs.inverted()
2423 };
2424 }
2425 }
2426 Some(TypeExpr::Named(ref n)) if n == "nil" => {
2427 let eq_refs = Refinements {
2429 truthy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
2430 falsy: vec![(name.clone(), Some(TypeExpr::Never))],
2431 };
2432 return if op == "==" {
2433 eq_refs
2434 } else {
2435 eq_refs.inverted()
2436 };
2437 }
2438 _ => {}
2439 }
2440 }
2441 Refinements::empty()
2442 }
2443
2444 fn extract_typeof_refinements(
2446 op: &str,
2447 left: &SNode,
2448 right: &SNode,
2449 scope: &TypeScope,
2450 ) -> Refinements {
2451 let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
2452 (extract_type_of_var(left), &right.node)
2453 {
2454 (var, tn.clone())
2455 } else if let (Node::StringLiteral(tn), Some(var)) =
2456 (&left.node, extract_type_of_var(right))
2457 {
2458 (var, tn.clone())
2459 } else {
2460 return Refinements::empty();
2461 };
2462
2463 const KNOWN_TYPES: &[&str] = &[
2464 "int", "string", "float", "bool", "nil", "list", "dict", "closure",
2465 ];
2466 if !KNOWN_TYPES.contains(&type_name.as_str()) {
2467 return Refinements::empty();
2468 }
2469
2470 let var_type = scope.get_var(&var_name).cloned().flatten();
2471 match var_type {
2472 Some(TypeExpr::Union(ref members)) => {
2473 let narrowed = narrow_to_single(members, &type_name);
2474 let remaining = remove_from_union(members, &type_name);
2475 if narrowed.is_some() || remaining.is_some() {
2476 let eq_refs = Refinements {
2477 truthy: narrowed
2478 .map(|n| vec![(var_name.clone(), Some(n))])
2479 .unwrap_or_default(),
2480 falsy: remaining
2481 .map(|r| vec![(var_name.clone(), Some(r))])
2482 .unwrap_or_default(),
2483 };
2484 return if op == "==" {
2485 eq_refs
2486 } else {
2487 eq_refs.inverted()
2488 };
2489 }
2490 }
2491 Some(TypeExpr::Named(ref n)) if n == &type_name => {
2492 let eq_refs = Refinements {
2495 truthy: vec![(var_name.clone(), Some(TypeExpr::Named(type_name)))],
2496 falsy: vec![(var_name.clone(), Some(TypeExpr::Never))],
2497 };
2498 return if op == "==" {
2499 eq_refs
2500 } else {
2501 eq_refs.inverted()
2502 };
2503 }
2504 _ => {}
2505 }
2506 Refinements::empty()
2507 }
2508
2509 fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
2511 if let Node::Identifier(var_name) = &object.node {
2512 if let Node::StringLiteral(key) = &args[0].node {
2513 if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
2514 if fields.iter().any(|f| f.name == *key && f.optional) {
2515 let narrowed_fields: Vec<ShapeField> = fields
2516 .iter()
2517 .map(|f| {
2518 if f.name == *key {
2519 ShapeField {
2520 name: f.name.clone(),
2521 type_expr: f.type_expr.clone(),
2522 optional: false,
2523 }
2524 } else {
2525 f.clone()
2526 }
2527 })
2528 .collect();
2529 return Refinements {
2530 truthy: vec![(
2531 var_name.clone(),
2532 Some(TypeExpr::Shape(narrowed_fields)),
2533 )],
2534 falsy: vec![],
2535 };
2536 }
2537 }
2538 }
2539 }
2540 Refinements::empty()
2541 }
2542
2543 fn extract_schema_refinements(args: &[SNode], scope: &TypeScope) -> Refinements {
2544 let Node::Identifier(var_name) = &args[0].node else {
2545 return Refinements::empty();
2546 };
2547 let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) else {
2548 return Refinements::empty();
2549 };
2550 let Some(Some(var_type)) = scope.get_var(var_name).cloned() else {
2551 return Refinements::empty();
2552 };
2553
2554 let truthy = intersect_types(&var_type, &schema_type)
2555 .map(|ty| vec![(var_name.clone(), Some(ty))])
2556 .unwrap_or_default();
2557 let falsy = subtract_type(&var_type, &schema_type)
2558 .map(|ty| vec![(var_name.clone(), Some(ty))])
2559 .unwrap_or_default();
2560
2561 Refinements { truthy, falsy }
2562 }
2563
2564 fn block_definitely_exits(stmts: &[SNode]) -> bool {
2566 block_definitely_exits(stmts)
2567 }
2568
2569 fn check_match_exhaustiveness(
2570 &mut self,
2571 value: &SNode,
2572 arms: &[MatchArm],
2573 scope: &TypeScope,
2574 span: Span,
2575 ) {
2576 let enum_name = match &value.node {
2578 Node::PropertyAccess { object, property } if property == "variant" => {
2579 match self.infer_type(object, scope) {
2581 Some(TypeExpr::Named(name)) => {
2582 if scope.get_enum(&name).is_some() {
2583 Some(name)
2584 } else {
2585 None
2586 }
2587 }
2588 _ => None,
2589 }
2590 }
2591 _ => {
2592 match self.infer_type(value, scope) {
2594 Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
2595 _ => None,
2596 }
2597 }
2598 };
2599
2600 let Some(enum_name) = enum_name else {
2601 self.check_match_exhaustiveness_union(value, arms, scope, span);
2603 return;
2604 };
2605 let Some(variants) = scope.get_enum(&enum_name) else {
2606 return;
2607 };
2608
2609 let mut covered: Vec<String> = Vec::new();
2611 let mut has_wildcard = false;
2612
2613 for arm in arms {
2614 match &arm.pattern.node {
2615 Node::StringLiteral(s) => covered.push(s.clone()),
2617 Node::Identifier(name)
2619 if name == "_"
2620 || !variants
2621 .variants
2622 .iter()
2623 .any(|variant| variant.name == *name) =>
2624 {
2625 has_wildcard = true;
2626 }
2627 Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
2629 Node::PropertyAccess { property, .. } => covered.push(property.clone()),
2631 _ => {
2632 has_wildcard = true;
2634 }
2635 }
2636 }
2637
2638 if has_wildcard {
2639 return;
2640 }
2641
2642 let missing: Vec<&String> = variants
2643 .variants
2644 .iter()
2645 .map(|variant| &variant.name)
2646 .filter(|variant| !covered.contains(variant))
2647 .collect();
2648 if !missing.is_empty() {
2649 let missing_str = missing
2650 .iter()
2651 .map(|s| format!("\"{}\"", s))
2652 .collect::<Vec<_>>()
2653 .join(", ");
2654 self.warning_at(
2655 format!(
2656 "Non-exhaustive match on enum {}: missing variants {}",
2657 enum_name, missing_str
2658 ),
2659 span,
2660 );
2661 }
2662 }
2663
2664 fn check_match_exhaustiveness_union(
2666 &mut self,
2667 value: &SNode,
2668 arms: &[MatchArm],
2669 scope: &TypeScope,
2670 span: Span,
2671 ) {
2672 let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
2673 return;
2674 };
2675 if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
2677 return;
2678 }
2679
2680 let mut has_wildcard = false;
2681 let mut covered_types: Vec<String> = Vec::new();
2682
2683 for arm in arms {
2684 match &arm.pattern.node {
2685 Node::NilLiteral => covered_types.push("nil".into()),
2688 Node::BoolLiteral(_) => {
2689 if !covered_types.contains(&"bool".into()) {
2690 covered_types.push("bool".into());
2691 }
2692 }
2693 Node::IntLiteral(_) => {
2694 if !covered_types.contains(&"int".into()) {
2695 covered_types.push("int".into());
2696 }
2697 }
2698 Node::FloatLiteral(_) => {
2699 if !covered_types.contains(&"float".into()) {
2700 covered_types.push("float".into());
2701 }
2702 }
2703 Node::StringLiteral(_) => {
2704 if !covered_types.contains(&"string".into()) {
2705 covered_types.push("string".into());
2706 }
2707 }
2708 Node::Identifier(name) if name == "_" => {
2709 has_wildcard = true;
2710 }
2711 _ => {
2712 has_wildcard = true;
2713 }
2714 }
2715 }
2716
2717 if has_wildcard {
2718 return;
2719 }
2720
2721 let type_names: Vec<&str> = members
2722 .iter()
2723 .filter_map(|m| match m {
2724 TypeExpr::Named(n) => Some(n.as_str()),
2725 _ => None,
2726 })
2727 .collect();
2728 let missing: Vec<&&str> = type_names
2729 .iter()
2730 .filter(|t| !covered_types.iter().any(|c| c == **t))
2731 .collect();
2732 if !missing.is_empty() {
2733 let missing_str = missing
2734 .iter()
2735 .map(|s| s.to_string())
2736 .collect::<Vec<_>>()
2737 .join(", ");
2738 self.warning_at(
2739 format!(
2740 "Non-exhaustive match on union type: missing {}",
2741 missing_str
2742 ),
2743 span,
2744 );
2745 }
2746 }
2747
2748 fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
2749 if name == "unreachable" {
2752 if let Some(arg) = args.first() {
2753 if matches!(&arg.node, Node::Identifier(_)) {
2754 let arg_type = self.infer_type(arg, scope);
2755 if let Some(ref ty) = arg_type {
2756 if !matches!(ty, TypeExpr::Never) {
2757 self.error_at(
2758 format!(
2759 "unreachable() argument has type `{}` — not all cases are handled",
2760 format_type(ty)
2761 ),
2762 span,
2763 );
2764 }
2765 }
2766 }
2767 }
2768 for arg in args {
2769 self.check_node(arg, scope);
2770 }
2771 return;
2772 }
2773
2774 let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
2776 if let Some(sig) = scope.get_fn(name).cloned() {
2777 if !has_spread
2778 && !is_builtin(name)
2779 && !sig.has_rest
2780 && (args.len() < sig.required_params || args.len() > sig.params.len())
2781 {
2782 let expected = if sig.required_params == sig.params.len() {
2783 format!("{}", sig.params.len())
2784 } else {
2785 format!("{}-{}", sig.required_params, sig.params.len())
2786 };
2787 self.warning_at(
2788 format!(
2789 "Function '{}' expects {} arguments, got {}",
2790 name,
2791 expected,
2792 args.len()
2793 ),
2794 span,
2795 );
2796 }
2797 let call_scope = if sig.type_param_names.is_empty() {
2800 scope.clone()
2801 } else {
2802 let mut s = scope.child();
2803 for tp_name in &sig.type_param_names {
2804 s.generic_type_params.insert(tp_name.clone());
2805 }
2806 s
2807 };
2808 let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
2809 let type_param_set: std::collections::BTreeSet<String> =
2810 sig.type_param_names.iter().cloned().collect();
2811 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2812 if let Some(param_ty) = param_type {
2813 if let Some(arg_ty) = self.infer_type(arg, scope) {
2814 if let Err(message) = Self::extract_type_bindings(
2815 param_ty,
2816 &arg_ty,
2817 &type_param_set,
2818 &mut type_bindings,
2819 ) {
2820 self.error_at(message, arg.span);
2821 }
2822 }
2823 }
2824 }
2825 for (i, (arg, (param_name, param_type))) in
2826 args.iter().zip(sig.params.iter()).enumerate()
2827 {
2828 if let Some(expected) = param_type {
2829 let actual = self.infer_type(arg, scope);
2830 if let Some(actual) = &actual {
2831 let expected = Self::apply_type_bindings(expected, &type_bindings);
2832 if !self.types_compatible(&expected, actual, &call_scope) {
2833 self.error_at(
2834 format!(
2835 "Argument {} ('{}'): expected {}, got {}",
2836 i + 1,
2837 param_name,
2838 format_type(&expected),
2839 format_type(actual)
2840 ),
2841 arg.span,
2842 );
2843 }
2844 }
2845 }
2846 }
2847 if !sig.where_clauses.is_empty() {
2848 for (type_param, bound) in &sig.where_clauses {
2849 if let Some(concrete_type) = type_bindings.get(type_param) {
2850 let concrete_name = format_type(concrete_type);
2851 let Some(base_type_name) = Self::base_type_name(concrete_type) else {
2852 self.error_at(
2853 format!(
2854 "Type '{}' does not satisfy interface '{}': only named types can satisfy interfaces (required by constraint `where {}: {}`)",
2855 concrete_name, bound, type_param, bound
2856 ),
2857 span,
2858 );
2859 continue;
2860 };
2861 if let Some(reason) = self.interface_mismatch_reason(
2862 base_type_name,
2863 bound,
2864 &BTreeMap::new(),
2865 scope,
2866 ) {
2867 self.error_at(
2868 format!(
2869 "Type '{}' does not satisfy interface '{}': {} \
2870 (required by constraint `where {}: {}`)",
2871 concrete_name, bound, reason, type_param, bound
2872 ),
2873 span,
2874 );
2875 }
2876 }
2877 }
2878 }
2879 }
2880 for arg in args {
2882 self.check_node(arg, scope);
2883 }
2884 }
2885
2886 fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
2888 match &snode.node {
2889 Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
2890 Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
2891 Node::StringLiteral(_) | Node::InterpolatedString(_) => {
2892 Some(TypeExpr::Named("string".into()))
2893 }
2894 Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
2895 Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
2896 Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
2897 Node::DictLiteral(entries) => {
2898 let mut fields = Vec::new();
2900 for entry in entries {
2901 let key = match &entry.key.node {
2902 Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
2903 _ => return Some(TypeExpr::Named("dict".into())),
2904 };
2905 let val_type = self
2906 .infer_type(&entry.value, scope)
2907 .unwrap_or(TypeExpr::Named("nil".into()));
2908 fields.push(ShapeField {
2909 name: key,
2910 type_expr: val_type,
2911 optional: false,
2912 });
2913 }
2914 if !fields.is_empty() {
2915 Some(TypeExpr::Shape(fields))
2916 } else {
2917 Some(TypeExpr::Named("dict".into()))
2918 }
2919 }
2920 Node::Closure { params, body, .. } => {
2921 let all_typed = params.iter().all(|p| p.type_expr.is_some());
2923 if all_typed && !params.is_empty() {
2924 let param_types: Vec<TypeExpr> =
2925 params.iter().filter_map(|p| p.type_expr.clone()).collect();
2926 let ret = body.last().and_then(|last| self.infer_type(last, scope));
2928 if let Some(ret_type) = ret {
2929 return Some(TypeExpr::FnType {
2930 params: param_types,
2931 return_type: Box::new(ret_type),
2932 });
2933 }
2934 }
2935 Some(TypeExpr::Named("closure".into()))
2936 }
2937
2938 Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
2939
2940 Node::FunctionCall { name, args } => {
2941 if let Some(struct_info) = scope.get_struct(name) {
2943 return Some(Self::applied_type_or_name(
2944 name,
2945 struct_info
2946 .type_params
2947 .iter()
2948 .map(|_| Self::wildcard_type())
2949 .collect(),
2950 ));
2951 }
2952 if name == "Ok" {
2953 let ok_type = args
2954 .first()
2955 .and_then(|arg| self.infer_type(arg, scope))
2956 .unwrap_or_else(Self::wildcard_type);
2957 return Some(TypeExpr::Applied {
2958 name: "Result".into(),
2959 args: vec![ok_type, Self::wildcard_type()],
2960 });
2961 }
2962 if name == "Err" {
2963 let err_type = args
2964 .first()
2965 .and_then(|arg| self.infer_type(arg, scope))
2966 .unwrap_or_else(Self::wildcard_type);
2967 return Some(TypeExpr::Applied {
2968 name: "Result".into(),
2969 args: vec![Self::wildcard_type(), err_type],
2970 });
2971 }
2972 if let Some(sig) = scope.get_fn(name) {
2974 let mut return_type = sig.return_type.clone();
2975 if let Some(ty) = return_type.take() {
2976 if sig.type_param_names.is_empty() {
2977 return Some(ty);
2978 }
2979 let mut bindings = BTreeMap::new();
2980 let type_param_set: std::collections::BTreeSet<String> =
2981 sig.type_param_names.iter().cloned().collect();
2982 for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2983 if let Some(param_ty) = param_type {
2984 if let Some(arg_ty) = self.infer_type(arg, scope) {
2985 let _ = Self::extract_type_bindings(
2986 param_ty,
2987 &arg_ty,
2988 &type_param_set,
2989 &mut bindings,
2990 );
2991 }
2992 }
2993 }
2994 return Some(Self::apply_type_bindings(&ty, &bindings));
2995 }
2996 return None;
2997 }
2998 if name == "schema_expect" && args.len() >= 2 {
3000 if let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) {
3001 return Some(schema_type);
3002 }
3003 }
3004 if (name == "schema_check" || name == "schema_parse") && args.len() >= 2 {
3005 if let Some(schema_type) = schema_type_expr_from_node(&args[1], scope) {
3006 return Some(TypeExpr::Applied {
3007 name: "Result".into(),
3008 args: vec![schema_type, TypeExpr::Named("string".into())],
3009 });
3010 }
3011 }
3012 if (name == "llm_call" || name == "llm_completion") && args.len() >= 3 {
3015 if let Some(schema_type) = Self::extract_llm_schema_from_options(args, scope) {
3016 return Some(TypeExpr::Shape(vec![
3017 ShapeField {
3018 name: "text".into(),
3019 type_expr: TypeExpr::Named("string".into()),
3020 optional: false,
3021 },
3022 ShapeField {
3023 name: "model".into(),
3024 type_expr: TypeExpr::Named("string".into()),
3025 optional: false,
3026 },
3027 ShapeField {
3028 name: "provider".into(),
3029 type_expr: TypeExpr::Named("string".into()),
3030 optional: false,
3031 },
3032 ShapeField {
3033 name: "input_tokens".into(),
3034 type_expr: TypeExpr::Named("int".into()),
3035 optional: false,
3036 },
3037 ShapeField {
3038 name: "output_tokens".into(),
3039 type_expr: TypeExpr::Named("int".into()),
3040 optional: false,
3041 },
3042 ShapeField {
3043 name: "data".into(),
3044 type_expr: schema_type,
3045 optional: false,
3046 },
3047 ShapeField {
3048 name: "visible_text".into(),
3049 type_expr: TypeExpr::Named("string".into()),
3050 optional: true,
3051 },
3052 ShapeField {
3053 name: "tool_calls".into(),
3054 type_expr: TypeExpr::Named("list".into()),
3055 optional: true,
3056 },
3057 ShapeField {
3058 name: "thinking".into(),
3059 type_expr: TypeExpr::Named("string".into()),
3060 optional: true,
3061 },
3062 ShapeField {
3063 name: "stop_reason".into(),
3064 type_expr: TypeExpr::Named("string".into()),
3065 optional: true,
3066 },
3067 ]));
3068 }
3069 }
3070 builtin_return_type(name)
3072 }
3073
3074 Node::BinaryOp { op, left, right } => {
3075 let lt = self.infer_type(left, scope);
3076 let rt = self.infer_type(right, scope);
3077 infer_binary_op_type(op, <, &rt)
3078 }
3079
3080 Node::UnaryOp { op, operand } => {
3081 let t = self.infer_type(operand, scope);
3082 match op.as_str() {
3083 "!" => Some(TypeExpr::Named("bool".into())),
3084 "-" => t, _ => None,
3086 }
3087 }
3088
3089 Node::Ternary {
3090 condition,
3091 true_expr,
3092 false_expr,
3093 } => {
3094 let refs = Self::extract_refinements(condition, scope);
3095
3096 let mut true_scope = scope.child();
3097 apply_refinements(&mut true_scope, &refs.truthy);
3098 let tt = self.infer_type(true_expr, &true_scope);
3099
3100 let mut false_scope = scope.child();
3101 apply_refinements(&mut false_scope, &refs.falsy);
3102 let ft = self.infer_type(false_expr, &false_scope);
3103
3104 match (&tt, &ft) {
3105 (Some(a), Some(b)) if a == b => tt,
3106 (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
3107 (Some(_), None) => tt,
3108 (None, Some(_)) => ft,
3109 (None, None) => None,
3110 }
3111 }
3112
3113 Node::EnumConstruct {
3114 enum_name,
3115 variant,
3116 args,
3117 } => {
3118 if let Some(enum_info) = scope.get_enum(enum_name) {
3119 Some(self.infer_enum_type(enum_name, enum_info, variant, args, scope))
3120 } else {
3121 Some(TypeExpr::Named(enum_name.clone()))
3122 }
3123 }
3124
3125 Node::PropertyAccess { object, property } => {
3126 if let Node::Identifier(name) = &object.node {
3128 if let Some(enum_info) = scope.get_enum(name) {
3129 return Some(self.infer_enum_type(name, enum_info, property, &[], scope));
3130 }
3131 }
3132 if property == "variant" {
3134 let obj_type = self.infer_type(object, scope);
3135 if let Some(name) = obj_type.as_ref().and_then(Self::base_type_name) {
3136 if scope.get_enum(name).is_some() {
3137 return Some(TypeExpr::Named("string".into()));
3138 }
3139 }
3140 }
3141 let obj_type = self.infer_type(object, scope);
3143 if let Some(TypeExpr::Shape(fields)) = &obj_type {
3144 if let Some(field) = fields.iter().find(|f| f.name == *property) {
3145 return Some(field.type_expr.clone());
3146 }
3147 }
3148 None
3149 }
3150
3151 Node::SubscriptAccess { object, index } => {
3152 let obj_type = self.infer_type(object, scope);
3153 match &obj_type {
3154 Some(TypeExpr::List(inner)) => Some(*inner.clone()),
3155 Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
3156 Some(TypeExpr::Shape(fields)) => {
3157 if let Node::StringLiteral(key) = &index.node {
3159 fields
3160 .iter()
3161 .find(|f| &f.name == key)
3162 .map(|f| f.type_expr.clone())
3163 } else {
3164 None
3165 }
3166 }
3167 Some(TypeExpr::Named(n)) if n == "list" => None,
3168 Some(TypeExpr::Named(n)) if n == "dict" => None,
3169 Some(TypeExpr::Named(n)) if n == "string" => {
3170 Some(TypeExpr::Named("string".into()))
3171 }
3172 _ => None,
3173 }
3174 }
3175 Node::SliceAccess { object, .. } => {
3176 let obj_type = self.infer_type(object, scope);
3178 match &obj_type {
3179 Some(TypeExpr::List(_)) => obj_type,
3180 Some(TypeExpr::Named(n)) if n == "list" => obj_type,
3181 Some(TypeExpr::Named(n)) if n == "string" => {
3182 Some(TypeExpr::Named("string".into()))
3183 }
3184 _ => None,
3185 }
3186 }
3187 Node::MethodCall {
3188 object,
3189 method,
3190 args,
3191 }
3192 | Node::OptionalMethodCall {
3193 object,
3194 method,
3195 args,
3196 } => {
3197 if let Node::Identifier(name) = &object.node {
3198 if let Some(enum_info) = scope.get_enum(name) {
3199 return Some(self.infer_enum_type(name, enum_info, method, args, scope));
3200 }
3201 if name == "Result" && (method == "Ok" || method == "Err") {
3202 let ok_type = if method == "Ok" {
3203 args.first()
3204 .and_then(|arg| self.infer_type(arg, scope))
3205 .unwrap_or_else(Self::wildcard_type)
3206 } else {
3207 Self::wildcard_type()
3208 };
3209 let err_type = if method == "Err" {
3210 args.first()
3211 .and_then(|arg| self.infer_type(arg, scope))
3212 .unwrap_or_else(Self::wildcard_type)
3213 } else {
3214 Self::wildcard_type()
3215 };
3216 return Some(TypeExpr::Applied {
3217 name: "Result".into(),
3218 args: vec![ok_type, err_type],
3219 });
3220 }
3221 }
3222 let obj_type = self.infer_type(object, scope);
3223 let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
3224 || matches!(&obj_type, Some(TypeExpr::DictType(..)))
3225 || matches!(&obj_type, Some(TypeExpr::Shape(_)));
3226 match method.as_str() {
3227 "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
3229 Some(TypeExpr::Named("bool".into()))
3230 }
3231 "count" | "index_of" => Some(TypeExpr::Named("int".into())),
3233 "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
3235 | "pad_left" | "pad_right" | "repeat" | "join" => {
3236 Some(TypeExpr::Named("string".into()))
3237 }
3238 "split" | "chars" => Some(TypeExpr::Named("list".into())),
3239 "filter" => {
3241 if is_dict {
3242 Some(TypeExpr::Named("dict".into()))
3243 } else {
3244 Some(TypeExpr::Named("list".into()))
3245 }
3246 }
3247 "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
3249 "window" | "each_cons" | "sliding_window" => match &obj_type {
3250 Some(TypeExpr::List(inner)) => Some(TypeExpr::List(Box::new(
3251 TypeExpr::List(Box::new((**inner).clone())),
3252 ))),
3253 _ => Some(TypeExpr::Named("list".into())),
3254 },
3255 "reduce" | "find" | "first" | "last" => None,
3256 "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
3258 "merge" | "map_values" | "rekey" | "map_keys" => {
3259 if let Some(TypeExpr::DictType(_, v)) = &obj_type {
3263 Some(TypeExpr::DictType(
3264 Box::new(TypeExpr::Named("string".into())),
3265 v.clone(),
3266 ))
3267 } else {
3268 Some(TypeExpr::Named("dict".into()))
3269 }
3270 }
3271 "to_string" => Some(TypeExpr::Named("string".into())),
3273 "to_int" => Some(TypeExpr::Named("int".into())),
3274 "to_float" => Some(TypeExpr::Named("float".into())),
3275 _ => None,
3276 }
3277 }
3278
3279 Node::TryOperator { operand } => match self.infer_type(operand, scope) {
3281 Some(TypeExpr::Applied { name, args }) if name == "Result" && args.len() == 2 => {
3282 Some(args[0].clone())
3283 }
3284 Some(TypeExpr::Named(name)) if name == "Result" => None,
3285 _ => None,
3286 },
3287
3288 Node::ThrowStmt { .. }
3290 | Node::ReturnStmt { .. }
3291 | Node::BreakStmt
3292 | Node::ContinueStmt => Some(TypeExpr::Never),
3293
3294 Node::IfElse {
3296 then_body,
3297 else_body,
3298 ..
3299 } => {
3300 let then_type = self.infer_block_type(then_body, scope);
3301 let else_type = else_body
3302 .as_ref()
3303 .and_then(|eb| self.infer_block_type(eb, scope));
3304 match (then_type, else_type) {
3305 (Some(TypeExpr::Never), Some(TypeExpr::Never)) => Some(TypeExpr::Never),
3306 (Some(TypeExpr::Never), Some(other)) | (Some(other), Some(TypeExpr::Never)) => {
3307 Some(other)
3308 }
3309 (Some(t), Some(e)) if t == e => Some(t),
3310 (Some(t), Some(e)) => Some(simplify_union(vec![t, e])),
3311 (Some(t), None) => Some(t),
3312 (None, _) => None,
3313 }
3314 }
3315
3316 Node::TryExpr { body } => {
3317 let ok_type = self
3318 .infer_block_type(body, scope)
3319 .unwrap_or_else(Self::wildcard_type);
3320 let err_type = self
3321 .infer_try_error_type(body, scope)
3322 .unwrap_or_else(Self::wildcard_type);
3323 Some(TypeExpr::Applied {
3324 name: "Result".into(),
3325 args: vec![ok_type, err_type],
3326 })
3327 }
3328
3329 Node::StructConstruct {
3330 struct_name,
3331 fields,
3332 } => scope
3333 .get_struct(struct_name)
3334 .map(|struct_info| self.infer_struct_type(struct_name, struct_info, fields, scope))
3335 .or_else(|| Some(TypeExpr::Named(struct_name.clone()))),
3336
3337 _ => None,
3338 }
3339 }
3340
3341 fn infer_block_type(&self, stmts: &[SNode], scope: &TypeScope) -> InferredType {
3343 if Self::block_definitely_exits(stmts) {
3344 return Some(TypeExpr::Never);
3345 }
3346 stmts.last().and_then(|s| self.infer_type(s, scope))
3347 }
3348
3349 fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
3351 if Self::is_wildcard_type(expected) || Self::is_wildcard_type(actual) {
3352 return true;
3353 }
3354 if let TypeExpr::Named(name) = expected {
3356 if scope.is_generic_type_param(name) {
3357 return true;
3358 }
3359 }
3360 if let TypeExpr::Named(name) = actual {
3361 if scope.is_generic_type_param(name) {
3362 return true;
3363 }
3364 }
3365 let expected = self.resolve_alias(expected, scope);
3366 let actual = self.resolve_alias(actual, scope);
3367
3368 if let Some(iface_name) = Self::base_type_name(&expected) {
3370 if let Some(interface_info) = scope.get_interface(iface_name) {
3371 let mut interface_bindings = BTreeMap::new();
3372 if let TypeExpr::Applied { args, .. } = &expected {
3373 for (type_param, arg) in interface_info.type_params.iter().zip(args.iter()) {
3374 interface_bindings.insert(type_param.clone(), arg.clone());
3375 }
3376 }
3377 if let Some(type_name) = Self::base_type_name(&actual) {
3378 return self.satisfies_interface(
3379 type_name,
3380 iface_name,
3381 &interface_bindings,
3382 scope,
3383 );
3384 }
3385 return false;
3386 }
3387 }
3388
3389 match (&expected, &actual) {
3390 (_, TypeExpr::Never) => true,
3392 (TypeExpr::Never, _) => false,
3394 (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
3395 (TypeExpr::Named(a), TypeExpr::Applied { name: b, .. })
3396 | (TypeExpr::Applied { name: a, .. }, TypeExpr::Named(b)) => a == b,
3397 (
3398 TypeExpr::Applied {
3399 name: expected_name,
3400 args: expected_args,
3401 },
3402 TypeExpr::Applied {
3403 name: actual_name,
3404 args: actual_args,
3405 },
3406 ) => {
3407 expected_name == actual_name
3408 && expected_args.len() == actual_args.len()
3409 && expected_args.iter().zip(actual_args.iter()).all(
3410 |(expected_arg, actual_arg)| {
3411 self.types_compatible(expected_arg, actual_arg, scope)
3412 },
3413 )
3414 }
3415 (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
3418 act_members.iter().all(|am| {
3419 exp_members
3420 .iter()
3421 .any(|em| self.types_compatible(em, am, scope))
3422 })
3423 }
3424 (TypeExpr::Union(members), actual_type) => members
3425 .iter()
3426 .any(|m| self.types_compatible(m, actual_type, scope)),
3427 (expected_type, TypeExpr::Union(members)) => members
3428 .iter()
3429 .all(|m| self.types_compatible(expected_type, m, scope)),
3430 (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
3431 (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
3432 (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
3433 if expected_field.optional {
3434 return true;
3435 }
3436 af.iter().any(|actual_field| {
3437 actual_field.name == expected_field.name
3438 && self.types_compatible(
3439 &expected_field.type_expr,
3440 &actual_field.type_expr,
3441 scope,
3442 )
3443 })
3444 }),
3445 (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
3447 let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
3448 keys_ok
3449 && af
3450 .iter()
3451 .all(|f| self.types_compatible(ev, &f.type_expr, scope))
3452 }
3453 (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
3455 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
3456 self.types_compatible(expected_inner, actual_inner, scope)
3457 }
3458 (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
3459 (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
3460 (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
3461 self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
3462 }
3463 (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
3464 (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
3465 (
3467 TypeExpr::FnType {
3468 params: ep,
3469 return_type: er,
3470 },
3471 TypeExpr::FnType {
3472 params: ap,
3473 return_type: ar,
3474 },
3475 ) => {
3476 ep.len() == ap.len()
3477 && ep
3478 .iter()
3479 .zip(ap.iter())
3480 .all(|(e, a)| self.types_compatible(e, a, scope))
3481 && self.types_compatible(er, ar, scope)
3482 }
3483 (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
3485 (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
3486 _ => false,
3487 }
3488 }
3489
3490 fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
3491 match ty {
3492 TypeExpr::Named(name) => {
3493 if let Some(resolved) = scope.resolve_type(name) {
3494 return self.resolve_alias(resolved, scope);
3495 }
3496 ty.clone()
3497 }
3498 TypeExpr::Union(types) => TypeExpr::Union(
3499 types
3500 .iter()
3501 .map(|ty| self.resolve_alias(ty, scope))
3502 .collect(),
3503 ),
3504 TypeExpr::Shape(fields) => TypeExpr::Shape(
3505 fields
3506 .iter()
3507 .map(|field| ShapeField {
3508 name: field.name.clone(),
3509 type_expr: self.resolve_alias(&field.type_expr, scope),
3510 optional: field.optional,
3511 })
3512 .collect(),
3513 ),
3514 TypeExpr::List(inner) => TypeExpr::List(Box::new(self.resolve_alias(inner, scope))),
3515 TypeExpr::DictType(key, value) => TypeExpr::DictType(
3516 Box::new(self.resolve_alias(key, scope)),
3517 Box::new(self.resolve_alias(value, scope)),
3518 ),
3519 TypeExpr::FnType {
3520 params,
3521 return_type,
3522 } => TypeExpr::FnType {
3523 params: params
3524 .iter()
3525 .map(|param| self.resolve_alias(param, scope))
3526 .collect(),
3527 return_type: Box::new(self.resolve_alias(return_type, scope)),
3528 },
3529 TypeExpr::Applied { name, args } => TypeExpr::Applied {
3530 name: name.clone(),
3531 args: args
3532 .iter()
3533 .map(|arg| self.resolve_alias(arg, scope))
3534 .collect(),
3535 },
3536 TypeExpr::Never => TypeExpr::Never,
3537 }
3538 }
3539
3540 fn error_at(&mut self, message: String, span: Span) {
3541 self.diagnostics.push(TypeDiagnostic {
3542 message,
3543 severity: DiagnosticSeverity::Error,
3544 span: Some(span),
3545 help: None,
3546 fix: None,
3547 });
3548 }
3549
3550 #[allow(dead_code)]
3551 fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
3552 self.diagnostics.push(TypeDiagnostic {
3553 message,
3554 severity: DiagnosticSeverity::Error,
3555 span: Some(span),
3556 help: Some(help),
3557 fix: None,
3558 });
3559 }
3560
3561 fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
3562 self.diagnostics.push(TypeDiagnostic {
3563 message,
3564 severity: DiagnosticSeverity::Error,
3565 span: Some(span),
3566 help: None,
3567 fix: Some(fix),
3568 });
3569 }
3570
3571 fn warning_at(&mut self, message: String, span: Span) {
3572 self.diagnostics.push(TypeDiagnostic {
3573 message,
3574 severity: DiagnosticSeverity::Warning,
3575 span: Some(span),
3576 help: None,
3577 fix: None,
3578 });
3579 }
3580
3581 #[allow(dead_code)]
3582 fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
3583 self.diagnostics.push(TypeDiagnostic {
3584 message,
3585 severity: DiagnosticSeverity::Warning,
3586 span: Some(span),
3587 help: Some(help),
3588 fix: None,
3589 });
3590 }
3591
3592 fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
3596 match &snode.node {
3597 Node::BinaryOp { op, left, right } => {
3598 self.check_binops(left, scope);
3599 self.check_binops(right, scope);
3600 let lt = self.infer_type(left, scope);
3601 let rt = self.infer_type(right, scope);
3602 if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (<, &rt) {
3603 let span = snode.span;
3604 match op.as_str() {
3605 "+" => {
3606 let valid = matches!(
3607 (l.as_str(), r.as_str()),
3608 ("int" | "float", "int" | "float")
3609 | ("string", "string")
3610 | ("list", "list")
3611 | ("dict", "dict")
3612 );
3613 if !valid {
3614 let msg =
3615 format!("Operator '+' is not valid for types {} and {}", l, r);
3616 let fix = if l == "string" || r == "string" {
3617 self.build_interpolation_fix(left, right, l == "string", span)
3618 } else {
3619 None
3620 };
3621 if let Some(fix) = fix {
3622 self.error_at_with_fix(msg, span, fix);
3623 } else {
3624 self.error_at(msg, span);
3625 }
3626 }
3627 }
3628 "-" | "/" | "%" | "**" => {
3629 let numeric = ["int", "float"];
3630 if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
3631 self.error_at(
3632 format!(
3633 "Operator '{}' requires numeric operands, got {} and {}",
3634 op, l, r
3635 ),
3636 span,
3637 );
3638 }
3639 }
3640 "*" => {
3641 let numeric = ["int", "float"];
3642 let is_numeric =
3643 numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
3644 let is_string_repeat =
3645 (l == "string" && r == "int") || (l == "int" && r == "string");
3646 if !is_numeric && !is_string_repeat {
3647 self.error_at(
3648 format!(
3649 "Operator '*' requires numeric operands or string * int, got {} and {}",
3650 l, r
3651 ),
3652 span,
3653 );
3654 }
3655 }
3656 _ => {}
3657 }
3658 }
3659 }
3660 Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
3662 _ => {}
3663 }
3664 }
3665
3666 fn build_interpolation_fix(
3668 &self,
3669 left: &SNode,
3670 right: &SNode,
3671 left_is_string: bool,
3672 expr_span: Span,
3673 ) -> Option<Vec<FixEdit>> {
3674 let src = self.source.as_ref()?;
3675 let (str_node, other_node) = if left_is_string {
3676 (left, right)
3677 } else {
3678 (right, left)
3679 };
3680 let str_text = src.get(str_node.span.start..str_node.span.end)?;
3681 let other_text = src.get(other_node.span.start..other_node.span.end)?;
3682 let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
3684 if other_text.contains('}') || other_text.contains('"') {
3686 return None;
3687 }
3688 let replacement = if left_is_string {
3689 format!("\"{inner}${{{other_text}}}\"")
3690 } else {
3691 format!("\"${{{other_text}}}{inner}\"")
3692 };
3693 Some(vec![FixEdit {
3694 span: expr_span,
3695 replacement,
3696 }])
3697 }
3698}
3699
3700impl Default for TypeChecker {
3701 fn default() -> Self {
3702 Self::new()
3703 }
3704}
3705
3706fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
3708 match op {
3709 "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
3710 Some(TypeExpr::Named("bool".into()))
3711 }
3712 "+" => match (left, right) {
3713 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3714 match (l.as_str(), r.as_str()) {
3715 ("int", "int") => Some(TypeExpr::Named("int".into())),
3716 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3717 ("string", "string") => Some(TypeExpr::Named("string".into())),
3718 ("list", "list") => Some(TypeExpr::Named("list".into())),
3719 ("dict", "dict") => Some(TypeExpr::Named("dict".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 ("int", "int") => Some(TypeExpr::Named("int".into())),
3729 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3730 _ => None,
3731 }
3732 }
3733 _ => None,
3734 },
3735 "**" => match (left, right) {
3736 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3737 match (l.as_str(), r.as_str()) {
3738 ("int", "int") => Some(TypeExpr::Named("int".into())),
3739 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3740 _ => None,
3741 }
3742 }
3743 _ => None,
3744 },
3745 "*" => match (left, right) {
3746 (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
3747 match (l.as_str(), r.as_str()) {
3748 ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
3749 ("int", "int") => Some(TypeExpr::Named("int".into())),
3750 ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
3751 _ => None,
3752 }
3753 }
3754 _ => None,
3755 },
3756 "??" => match (left, right) {
3757 (Some(TypeExpr::Union(members)), _) => {
3759 let non_nil: Vec<_> = members
3760 .iter()
3761 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
3762 .cloned()
3763 .collect();
3764 if non_nil.len() == 1 {
3765 Some(non_nil[0].clone())
3766 } else if non_nil.is_empty() {
3767 right.clone()
3768 } else {
3769 Some(TypeExpr::Union(non_nil))
3770 }
3771 }
3772 (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
3774 (Some(l), _) => Some(l.clone()),
3776 (None, _) => right.clone(),
3778 },
3779 "|>" => None,
3780 _ => None,
3781 }
3782}
3783
3784pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
3789 if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
3790 let mut details = Vec::new();
3791 for field in ef {
3792 if field.optional {
3793 continue;
3794 }
3795 match af.iter().find(|f| f.name == field.name) {
3796 None => details.push(format!(
3797 "missing field '{}' ({})",
3798 field.name,
3799 format_type(&field.type_expr)
3800 )),
3801 Some(actual_field) => {
3802 let e_str = format_type(&field.type_expr);
3803 let a_str = format_type(&actual_field.type_expr);
3804 if e_str != a_str {
3805 details.push(format!(
3806 "field '{}' has type {}, expected {}",
3807 field.name, a_str, e_str
3808 ));
3809 }
3810 }
3811 }
3812 }
3813 if details.is_empty() {
3814 None
3815 } else {
3816 Some(details.join("; "))
3817 }
3818 } else {
3819 None
3820 }
3821}
3822
3823fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
3826 matches!(
3827 &value.node,
3828 Node::IntLiteral(_)
3829 | Node::FloatLiteral(_)
3830 | Node::StringLiteral(_)
3831 | Node::BoolLiteral(_)
3832 | Node::NilLiteral
3833 | Node::ListLiteral(_)
3834 | Node::DictLiteral(_)
3835 | Node::InterpolatedString(_)
3836 )
3837}
3838
3839pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
3842 match &stmt.node {
3843 Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
3844 true
3845 }
3846 Node::IfElse {
3847 then_body,
3848 else_body: Some(else_body),
3849 ..
3850 } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
3851 _ => false,
3852 }
3853}
3854
3855pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
3857 stmts.iter().any(stmt_definitely_exits)
3858}
3859
3860pub fn format_type(ty: &TypeExpr) -> String {
3861 match ty {
3862 TypeExpr::Named(n) => n.clone(),
3863 TypeExpr::Union(types) => types
3864 .iter()
3865 .map(format_type)
3866 .collect::<Vec<_>>()
3867 .join(" | "),
3868 TypeExpr::Shape(fields) => {
3869 let inner: Vec<String> = fields
3870 .iter()
3871 .map(|f| {
3872 let opt = if f.optional { "?" } else { "" };
3873 format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
3874 })
3875 .collect();
3876 format!("{{{}}}", inner.join(", "))
3877 }
3878 TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
3879 TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
3880 TypeExpr::Applied { name, args } => {
3881 let args_str = args.iter().map(format_type).collect::<Vec<_>>().join(", ");
3882 format!("{name}<{args_str}>")
3883 }
3884 TypeExpr::FnType {
3885 params,
3886 return_type,
3887 } => {
3888 let params_str = params
3889 .iter()
3890 .map(format_type)
3891 .collect::<Vec<_>>()
3892 .join(", ");
3893 format!("fn({}) -> {}", params_str, format_type(return_type))
3894 }
3895 TypeExpr::Never => "never".to_string(),
3896 }
3897}
3898
3899fn simplify_union(members: Vec<TypeExpr>) -> TypeExpr {
3901 let filtered: Vec<TypeExpr> = members
3902 .into_iter()
3903 .filter(|m| !matches!(m, TypeExpr::Never))
3904 .collect();
3905 match filtered.len() {
3906 0 => TypeExpr::Never,
3907 1 => filtered.into_iter().next().unwrap(),
3908 _ => TypeExpr::Union(filtered),
3909 }
3910}
3911
3912fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
3915 let remaining: Vec<TypeExpr> = members
3916 .iter()
3917 .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
3918 .cloned()
3919 .collect();
3920 match remaining.len() {
3921 0 => Some(TypeExpr::Never),
3922 1 => Some(remaining.into_iter().next().unwrap()),
3923 _ => Some(TypeExpr::Union(remaining)),
3924 }
3925}
3926
3927fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
3929 if members
3930 .iter()
3931 .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
3932 {
3933 Some(TypeExpr::Named(target.to_string()))
3934 } else {
3935 None
3936 }
3937}
3938
3939fn extract_type_of_var(node: &SNode) -> Option<String> {
3941 if let Node::FunctionCall { name, args } = &node.node {
3942 if name == "type_of" && args.len() == 1 {
3943 if let Node::Identifier(var) = &args[0].node {
3944 return Some(var.clone());
3945 }
3946 }
3947 }
3948 None
3949}
3950
3951fn schema_type_expr_from_node(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
3952 match &node.node {
3953 Node::Identifier(name) => scope.get_schema_binding(name).cloned().flatten(),
3954 Node::DictLiteral(entries) => schema_type_expr_from_dict(entries, scope),
3955 _ => None,
3956 }
3957}
3958
3959fn schema_type_expr_from_dict(entries: &[DictEntry], scope: &TypeScope) -> Option<TypeExpr> {
3960 let mut type_name: Option<String> = None;
3961 let mut properties: Option<&SNode> = None;
3962 let mut required: Option<Vec<String>> = None;
3963 let mut items: Option<&SNode> = None;
3964 let mut union: Option<&SNode> = None;
3965 let mut nullable = false;
3966 let mut additional_properties: Option<&SNode> = None;
3967
3968 for entry in entries {
3969 let key = schema_entry_key(&entry.key)?;
3970 match key.as_str() {
3971 "type" => match &entry.value.node {
3972 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3973 type_name = Some(normalize_schema_type_name(text));
3974 }
3975 Node::ListLiteral(items_list) => {
3976 let union_members = items_list
3977 .iter()
3978 .filter_map(|item| match &item.node {
3979 Node::StringLiteral(text) | Node::RawStringLiteral(text) => {
3980 Some(TypeExpr::Named(normalize_schema_type_name(text)))
3981 }
3982 _ => None,
3983 })
3984 .collect::<Vec<_>>();
3985 if !union_members.is_empty() {
3986 return Some(TypeExpr::Union(union_members));
3987 }
3988 }
3989 _ => {}
3990 },
3991 "properties" => properties = Some(&entry.value),
3992 "required" => {
3993 required = schema_required_names(&entry.value);
3994 }
3995 "items" => items = Some(&entry.value),
3996 "union" | "oneOf" | "anyOf" => union = Some(&entry.value),
3997 "nullable" => {
3998 nullable = matches!(entry.value.node, Node::BoolLiteral(true));
3999 }
4000 "additional_properties" | "additionalProperties" => {
4001 additional_properties = Some(&entry.value);
4002 }
4003 _ => {}
4004 }
4005 }
4006
4007 let mut schema_type = if let Some(union_node) = union {
4008 schema_union_type_expr(union_node, scope)?
4009 } else if let Some(properties_node) = properties {
4010 let property_entries = match &properties_node.node {
4011 Node::DictLiteral(entries) => entries,
4012 _ => return None,
4013 };
4014 let required_names = required.unwrap_or_default();
4015 let mut fields = Vec::new();
4016 for entry in property_entries {
4017 let field_name = schema_entry_key(&entry.key)?;
4018 let field_type = schema_type_expr_from_node(&entry.value, scope)?;
4019 fields.push(ShapeField {
4020 name: field_name.clone(),
4021 type_expr: field_type,
4022 optional: !required_names.contains(&field_name),
4023 });
4024 }
4025 TypeExpr::Shape(fields)
4026 } else if let Some(item_node) = items {
4027 TypeExpr::List(Box::new(schema_type_expr_from_node(item_node, scope)?))
4028 } else if let Some(type_name) = type_name {
4029 if type_name == "dict" {
4030 if let Some(extra_node) = additional_properties {
4031 let value_type = match &extra_node.node {
4032 Node::BoolLiteral(_) => None,
4033 _ => schema_type_expr_from_node(extra_node, scope),
4034 };
4035 if let Some(value_type) = value_type {
4036 TypeExpr::DictType(
4037 Box::new(TypeExpr::Named("string".into())),
4038 Box::new(value_type),
4039 )
4040 } else {
4041 TypeExpr::Named(type_name)
4042 }
4043 } else {
4044 TypeExpr::Named(type_name)
4045 }
4046 } else {
4047 TypeExpr::Named(type_name)
4048 }
4049 } else {
4050 return None;
4051 };
4052
4053 if nullable {
4054 schema_type = match schema_type {
4055 TypeExpr::Union(mut members) => {
4056 if !members
4057 .iter()
4058 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil"))
4059 {
4060 members.push(TypeExpr::Named("nil".into()));
4061 }
4062 TypeExpr::Union(members)
4063 }
4064 other => TypeExpr::Union(vec![other, TypeExpr::Named("nil".into())]),
4065 };
4066 }
4067
4068 Some(schema_type)
4069}
4070
4071fn schema_union_type_expr(node: &SNode, scope: &TypeScope) -> Option<TypeExpr> {
4072 let Node::ListLiteral(items) = &node.node else {
4073 return None;
4074 };
4075 let members = items
4076 .iter()
4077 .filter_map(|item| schema_type_expr_from_node(item, scope))
4078 .collect::<Vec<_>>();
4079 match members.len() {
4080 0 => None,
4081 1 => members.into_iter().next(),
4082 _ => Some(TypeExpr::Union(members)),
4083 }
4084}
4085
4086fn schema_required_names(node: &SNode) -> Option<Vec<String>> {
4087 let Node::ListLiteral(items) = &node.node else {
4088 return None;
4089 };
4090 Some(
4091 items
4092 .iter()
4093 .filter_map(|item| match &item.node {
4094 Node::StringLiteral(text) | Node::RawStringLiteral(text) => Some(text.clone()),
4095 Node::Identifier(text) => Some(text.clone()),
4096 _ => None,
4097 })
4098 .collect(),
4099 )
4100}
4101
4102fn schema_entry_key(node: &SNode) -> Option<String> {
4103 match &node.node {
4104 Node::Identifier(name) => Some(name.clone()),
4105 Node::StringLiteral(name) | Node::RawStringLiteral(name) => Some(name.clone()),
4106 _ => None,
4107 }
4108}
4109
4110fn normalize_schema_type_name(text: &str) -> String {
4111 match text {
4112 "object" => "dict".into(),
4113 "array" => "list".into(),
4114 "integer" => "int".into(),
4115 "number" => "float".into(),
4116 "boolean" => "bool".into(),
4117 "null" => "nil".into(),
4118 other => other.into(),
4119 }
4120}
4121
4122fn intersect_types(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
4123 match (current, schema_type) {
4124 (TypeExpr::Union(members), other) => {
4125 let kept = members
4126 .iter()
4127 .filter_map(|member| intersect_types(member, other))
4128 .collect::<Vec<_>>();
4129 match kept.len() {
4130 0 => None,
4131 1 => kept.into_iter().next(),
4132 _ => Some(TypeExpr::Union(kept)),
4133 }
4134 }
4135 (other, TypeExpr::Union(members)) => {
4136 let kept = members
4137 .iter()
4138 .filter_map(|member| intersect_types(other, member))
4139 .collect::<Vec<_>>();
4140 match kept.len() {
4141 0 => None,
4142 1 => kept.into_iter().next(),
4143 _ => Some(TypeExpr::Union(kept)),
4144 }
4145 }
4146 (TypeExpr::Named(left), TypeExpr::Named(right)) if left == right => {
4147 Some(TypeExpr::Named(left.clone()))
4148 }
4149 (TypeExpr::Named(name), TypeExpr::Shape(fields)) if name == "dict" => {
4150 Some(TypeExpr::Shape(fields.clone()))
4151 }
4152 (TypeExpr::Shape(fields), TypeExpr::Named(name)) if name == "dict" => {
4153 Some(TypeExpr::Shape(fields.clone()))
4154 }
4155 (TypeExpr::Named(name), TypeExpr::List(inner)) if name == "list" => {
4156 Some(TypeExpr::List(inner.clone()))
4157 }
4158 (TypeExpr::List(inner), TypeExpr::Named(name)) if name == "list" => {
4159 Some(TypeExpr::List(inner.clone()))
4160 }
4161 (TypeExpr::Named(name), TypeExpr::DictType(key, value)) if name == "dict" => {
4162 Some(TypeExpr::DictType(key.clone(), value.clone()))
4163 }
4164 (TypeExpr::DictType(key, value), TypeExpr::Named(name)) if name == "dict" => {
4165 Some(TypeExpr::DictType(key.clone(), value.clone()))
4166 }
4167 (TypeExpr::Shape(_), TypeExpr::Shape(fields)) => Some(TypeExpr::Shape(fields.clone())),
4168 (TypeExpr::List(current_inner), TypeExpr::List(schema_inner)) => {
4169 intersect_types(current_inner, schema_inner)
4170 .map(|inner| TypeExpr::List(Box::new(inner)))
4171 }
4172 (
4173 TypeExpr::DictType(current_key, current_value),
4174 TypeExpr::DictType(schema_key, schema_value),
4175 ) => {
4176 let key = intersect_types(current_key, schema_key)?;
4177 let value = intersect_types(current_value, schema_value)?;
4178 Some(TypeExpr::DictType(Box::new(key), Box::new(value)))
4179 }
4180 _ => None,
4181 }
4182}
4183
4184fn subtract_type(current: &TypeExpr, schema_type: &TypeExpr) -> Option<TypeExpr> {
4185 match current {
4186 TypeExpr::Union(members) => {
4187 let remaining = members
4188 .iter()
4189 .filter(|member| intersect_types(member, schema_type).is_none())
4190 .cloned()
4191 .collect::<Vec<_>>();
4192 match remaining.len() {
4193 0 => None,
4194 1 => remaining.into_iter().next(),
4195 _ => Some(TypeExpr::Union(remaining)),
4196 }
4197 }
4198 other if intersect_types(other, schema_type).is_some() => None,
4199 other => Some(other.clone()),
4200 }
4201}
4202
4203fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
4205 for (var_name, narrowed_type) in refinements {
4206 if !scope.narrowed_vars.contains_key(var_name) {
4208 if let Some(original) = scope.get_var(var_name).cloned() {
4209 scope.narrowed_vars.insert(var_name.clone(), original);
4210 }
4211 }
4212 scope.define_var(var_name, narrowed_type.clone());
4213 }
4214}
4215
4216#[cfg(test)]
4217mod tests {
4218 use super::*;
4219 use crate::Parser;
4220 use harn_lexer::Lexer;
4221
4222 fn check_source(source: &str) -> Vec<TypeDiagnostic> {
4223 let mut lexer = Lexer::new(source);
4224 let tokens = lexer.tokenize().unwrap();
4225 let mut parser = Parser::new(tokens);
4226 let program = parser.parse().unwrap();
4227 TypeChecker::new().check(&program)
4228 }
4229
4230 fn errors(source: &str) -> Vec<String> {
4231 check_source(source)
4232 .into_iter()
4233 .filter(|d| d.severity == DiagnosticSeverity::Error)
4234 .map(|d| d.message)
4235 .collect()
4236 }
4237
4238 #[test]
4239 fn test_no_errors_for_untyped_code() {
4240 let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
4241 assert!(errs.is_empty());
4242 }
4243
4244 #[test]
4245 fn test_correct_typed_let() {
4246 let errs = errors("pipeline t(task) { let x: int = 42 }");
4247 assert!(errs.is_empty());
4248 }
4249
4250 #[test]
4251 fn test_type_mismatch_let() {
4252 let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
4253 assert_eq!(errs.len(), 1);
4254 assert!(errs[0].contains("Type mismatch"));
4255 assert!(errs[0].contains("int"));
4256 assert!(errs[0].contains("string"));
4257 }
4258
4259 #[test]
4260 fn test_correct_typed_fn() {
4261 let errs = errors(
4262 "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
4263 );
4264 assert!(errs.is_empty());
4265 }
4266
4267 #[test]
4268 fn test_fn_arg_type_mismatch() {
4269 let errs = errors(
4270 r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
4271add("hello", 2) }"#,
4272 );
4273 assert_eq!(errs.len(), 1);
4274 assert!(errs[0].contains("Argument 1"));
4275 assert!(errs[0].contains("expected int"));
4276 }
4277
4278 #[test]
4279 fn test_return_type_mismatch() {
4280 let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
4281 assert_eq!(errs.len(), 1);
4282 assert!(errs[0].contains("Return type mismatch"));
4283 }
4284
4285 #[test]
4286 fn test_union_type_compatible() {
4287 let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
4288 assert!(errs.is_empty());
4289 }
4290
4291 #[test]
4292 fn test_union_type_mismatch() {
4293 let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
4294 assert_eq!(errs.len(), 1);
4295 assert!(errs[0].contains("Type mismatch"));
4296 }
4297
4298 #[test]
4299 fn test_type_inference_propagation() {
4300 let errs = errors(
4301 r#"pipeline t(task) {
4302 fn add(a: int, b: int) -> int { return a + b }
4303 let result: string = add(1, 2)
4304}"#,
4305 );
4306 assert_eq!(errs.len(), 1);
4307 assert!(errs[0].contains("Type mismatch"));
4308 assert!(errs[0].contains("string"));
4309 assert!(errs[0].contains("int"));
4310 }
4311
4312 #[test]
4313 fn test_generic_return_type_instantiates_from_callsite() {
4314 let errs = errors(
4315 r#"pipeline t(task) {
4316 fn identity<T>(x: T) -> T { return x }
4317 fn first<T>(items: list<T>) -> T { return items[0] }
4318 let n: int = identity(42)
4319 let s: string = first(["a", "b"])
4320}"#,
4321 );
4322 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4323 }
4324
4325 #[test]
4326 fn test_generic_type_param_must_bind_consistently() {
4327 let errs = errors(
4328 r#"pipeline t(task) {
4329 fn keep<T>(a: T, b: T) -> T { return a }
4330 keep(1, "x")
4331}"#,
4332 );
4333 assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
4334 assert!(
4335 errs.iter()
4336 .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
4337 "missing generic binding conflict error: {:?}",
4338 errs
4339 );
4340 assert!(
4341 errs.iter()
4342 .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
4343 "missing instantiated argument mismatch error: {:?}",
4344 errs
4345 );
4346 }
4347
4348 #[test]
4349 fn test_generic_list_binding_propagates_element_type() {
4350 let errs = errors(
4351 r#"pipeline t(task) {
4352 fn first<T>(items: list<T>) -> T { return items[0] }
4353 let bad: string = first([1, 2, 3])
4354}"#,
4355 );
4356 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
4357 assert!(errs[0].contains("declared as string, but assigned int"));
4358 }
4359
4360 #[test]
4361 fn test_generic_struct_literal_instantiates_type_arguments() {
4362 let errs = errors(
4363 r#"pipeline t(task) {
4364 struct Pair<A, B> {
4365 first: A
4366 second: B
4367 }
4368 let pair: Pair<int, string> = Pair { first: 1, second: "two" }
4369}"#,
4370 );
4371 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4372 }
4373
4374 #[test]
4375 fn test_generic_enum_construct_instantiates_type_arguments() {
4376 let errs = errors(
4377 r#"pipeline t(task) {
4378 enum Option<T> {
4379 Some(value: T),
4380 None
4381 }
4382 let value: Option<int> = Option.Some(42)
4383}"#,
4384 );
4385 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4386 }
4387
4388 #[test]
4389 fn test_result_generic_type_compatibility() {
4390 let errs = errors(
4391 r#"pipeline t(task) {
4392 let ok: Result<int, string> = Result.Ok(42)
4393 let err: Result<int, string> = Result.Err("oops")
4394}"#,
4395 );
4396 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4397 }
4398
4399 #[test]
4400 fn test_result_generic_type_mismatch_reports_error() {
4401 let errs = errors(
4402 r#"pipeline t(task) {
4403 let bad: Result<int, string> = Result.Err(42)
4404}"#,
4405 );
4406 assert_eq!(errs.len(), 1, "expected 1 error, got: {errs:?}");
4407 assert!(errs[0].contains("Result<int, string>"));
4408 assert!(errs[0].contains("Result<_, int>"));
4409 }
4410
4411 #[test]
4412 fn test_builtin_return_type_inference() {
4413 let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
4414 assert_eq!(errs.len(), 1);
4415 assert!(errs[0].contains("string"));
4416 assert!(errs[0].contains("int"));
4417 }
4418
4419 #[test]
4420 fn test_workflow_and_transcript_builtins_are_known() {
4421 let errs = errors(
4422 r#"pipeline t(task) {
4423 let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
4424 let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
4425 let run: dict = workflow_execute("task", flow, [], {})
4426 let tree: dict = load_run_tree("run.json")
4427 let fixture: dict = run_record_fixture(run?.run)
4428 let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
4429 let diff: dict = run_record_diff(run?.run, run?.run)
4430 let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
4431 let suite_report: dict = eval_suite_run(manifest)
4432 let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
4433 let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
4434 let selection: dict = artifact_editor_selection("src/main.rs", "main")
4435 let verify: dict = artifact_verification_result("verify", "ok")
4436 let test_result: dict = artifact_test_result("tests", "pass")
4437 let cmd: dict = artifact_command_result("cargo test", {status: 0})
4438 let patch: dict = artifact_diff("src/main.rs", "old", "new")
4439 let git: dict = artifact_git_diff("diff --git a b")
4440 let review: dict = artifact_diff_review(patch, "review me")
4441 let decision: dict = artifact_review_decision(review, "accepted")
4442 let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
4443 let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
4444 let apply: dict = artifact_apply_intent(review, "apply")
4445 let transcript = transcript_reset({metadata: {source: "test"}})
4446 let visible: string = transcript_render_visible(transcript_archive(transcript))
4447 let events: list = transcript_events(transcript)
4448 let context: string = artifact_context([], {max_artifacts: 1})
4449 println(report)
4450 println(run)
4451 println(tree)
4452 println(fixture)
4453 println(suite)
4454 println(diff)
4455 println(manifest)
4456 println(suite_report)
4457 println(wf)
4458 println(snap)
4459 println(selection)
4460 println(verify)
4461 println(test_result)
4462 println(cmd)
4463 println(patch)
4464 println(git)
4465 println(review)
4466 println(decision)
4467 println(proposal)
4468 println(bundle)
4469 println(apply)
4470 println(visible)
4471 println(events)
4472 println(context)
4473}"#,
4474 );
4475 assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
4476 }
4477
4478 #[test]
4479 fn test_binary_op_type_inference() {
4480 let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
4481 assert_eq!(errs.len(), 1);
4482 }
4483
4484 #[test]
4485 fn test_exponentiation_requires_numeric_operands() {
4486 let errs = errors(r#"pipeline t(task) { let x = "nope" ** 2 }"#);
4487 assert!(
4488 errs.iter()
4489 .any(|err| err.contains("Operator '**' requires numeric operands")),
4490 "missing exponentiation type error: {errs:?}"
4491 );
4492 }
4493
4494 #[test]
4495 fn test_comparison_returns_bool() {
4496 let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
4497 assert!(errs.is_empty());
4498 }
4499
4500 #[test]
4501 fn test_int_float_promotion() {
4502 let errs = errors("pipeline t(task) { let x: float = 42 }");
4503 assert!(errs.is_empty());
4504 }
4505
4506 #[test]
4507 fn test_untyped_code_no_errors() {
4508 let errs = errors(
4509 r#"pipeline t(task) {
4510 fn process(data) {
4511 let result = data + " processed"
4512 return result
4513 }
4514 log(process("hello"))
4515}"#,
4516 );
4517 assert!(errs.is_empty());
4518 }
4519
4520 #[test]
4521 fn test_type_alias() {
4522 let errs = errors(
4523 r#"pipeline t(task) {
4524 type Name = string
4525 let x: Name = "hello"
4526}"#,
4527 );
4528 assert!(errs.is_empty());
4529 }
4530
4531 #[test]
4532 fn test_type_alias_mismatch() {
4533 let errs = errors(
4534 r#"pipeline t(task) {
4535 type Name = string
4536 let x: Name = 42
4537}"#,
4538 );
4539 assert_eq!(errs.len(), 1);
4540 }
4541
4542 #[test]
4543 fn test_assignment_type_check() {
4544 let errs = errors(
4545 r#"pipeline t(task) {
4546 var x: int = 0
4547 x = "hello"
4548}"#,
4549 );
4550 assert_eq!(errs.len(), 1);
4551 assert!(errs[0].contains("cannot assign string"));
4552 }
4553
4554 #[test]
4555 fn test_covariance_int_to_float_in_fn() {
4556 let errs = errors(
4557 "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
4558 );
4559 assert!(errs.is_empty());
4560 }
4561
4562 #[test]
4563 fn test_covariance_return_type() {
4564 let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
4565 assert!(errs.is_empty());
4566 }
4567
4568 #[test]
4569 fn test_no_contravariance_float_to_int() {
4570 let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
4571 assert_eq!(errs.len(), 1);
4572 }
4573
4574 fn warnings(source: &str) -> Vec<String> {
4577 check_source(source)
4578 .into_iter()
4579 .filter(|d| d.severity == DiagnosticSeverity::Warning)
4580 .map(|d| d.message)
4581 .collect()
4582 }
4583
4584 #[test]
4585 fn test_exhaustive_match_no_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 "Blue" -> { log("b") }
4594 }
4595}"#,
4596 );
4597 let exhaustive_warns: Vec<_> = warns
4598 .iter()
4599 .filter(|w| w.contains("Non-exhaustive"))
4600 .collect();
4601 assert!(exhaustive_warns.is_empty());
4602 }
4603
4604 #[test]
4605 fn test_non_exhaustive_match_warning() {
4606 let warns = warnings(
4607 r#"pipeline t(task) {
4608 enum Color { Red, Green, Blue }
4609 let c = Color.Red
4610 match c.variant {
4611 "Red" -> { log("r") }
4612 "Green" -> { log("g") }
4613 }
4614}"#,
4615 );
4616 let exhaustive_warns: Vec<_> = warns
4617 .iter()
4618 .filter(|w| w.contains("Non-exhaustive"))
4619 .collect();
4620 assert_eq!(exhaustive_warns.len(), 1);
4621 assert!(exhaustive_warns[0].contains("Blue"));
4622 }
4623
4624 #[test]
4625 fn test_non_exhaustive_multiple_missing() {
4626 let warns = warnings(
4627 r#"pipeline t(task) {
4628 enum Status { Active, Inactive, Pending }
4629 let s = Status.Active
4630 match s.variant {
4631 "Active" -> { log("a") }
4632 }
4633}"#,
4634 );
4635 let exhaustive_warns: Vec<_> = warns
4636 .iter()
4637 .filter(|w| w.contains("Non-exhaustive"))
4638 .collect();
4639 assert_eq!(exhaustive_warns.len(), 1);
4640 assert!(exhaustive_warns[0].contains("Inactive"));
4641 assert!(exhaustive_warns[0].contains("Pending"));
4642 }
4643
4644 #[test]
4645 fn test_enum_construct_type_inference() {
4646 let errs = errors(
4647 r#"pipeline t(task) {
4648 enum Color { Red, Green, Blue }
4649 let c: Color = Color.Red
4650}"#,
4651 );
4652 assert!(errs.is_empty());
4653 }
4654
4655 #[test]
4658 fn test_nil_coalescing_strips_nil() {
4659 let errs = errors(
4661 r#"pipeline t(task) {
4662 let x: string | nil = nil
4663 let y: string = x ?? "default"
4664}"#,
4665 );
4666 assert!(errs.is_empty());
4667 }
4668
4669 #[test]
4670 fn test_shape_mismatch_detail_missing_field() {
4671 let errs = errors(
4672 r#"pipeline t(task) {
4673 let x: {name: string, age: int} = {name: "hello"}
4674}"#,
4675 );
4676 assert_eq!(errs.len(), 1);
4677 assert!(
4678 errs[0].contains("missing field 'age'"),
4679 "expected detail about missing field, got: {}",
4680 errs[0]
4681 );
4682 }
4683
4684 #[test]
4685 fn test_shape_mismatch_detail_wrong_type() {
4686 let errs = errors(
4687 r#"pipeline t(task) {
4688 let x: {name: string, age: int} = {name: 42, age: 10}
4689}"#,
4690 );
4691 assert_eq!(errs.len(), 1);
4692 assert!(
4693 errs[0].contains("field 'name' has type int, expected string"),
4694 "expected detail about wrong type, got: {}",
4695 errs[0]
4696 );
4697 }
4698
4699 #[test]
4702 fn test_match_pattern_string_against_int() {
4703 let warns = warnings(
4704 r#"pipeline t(task) {
4705 let x: int = 42
4706 match x {
4707 "hello" -> { log("bad") }
4708 42 -> { log("ok") }
4709 }
4710}"#,
4711 );
4712 let pattern_warns: Vec<_> = warns
4713 .iter()
4714 .filter(|w| w.contains("Match pattern type mismatch"))
4715 .collect();
4716 assert_eq!(pattern_warns.len(), 1);
4717 assert!(pattern_warns[0].contains("matching int against string literal"));
4718 }
4719
4720 #[test]
4721 fn test_match_pattern_int_against_string() {
4722 let warns = warnings(
4723 r#"pipeline t(task) {
4724 let x: string = "hello"
4725 match x {
4726 42 -> { log("bad") }
4727 "hello" -> { log("ok") }
4728 }
4729}"#,
4730 );
4731 let pattern_warns: Vec<_> = warns
4732 .iter()
4733 .filter(|w| w.contains("Match pattern type mismatch"))
4734 .collect();
4735 assert_eq!(pattern_warns.len(), 1);
4736 assert!(pattern_warns[0].contains("matching string against int literal"));
4737 }
4738
4739 #[test]
4740 fn test_match_pattern_bool_against_int() {
4741 let warns = warnings(
4742 r#"pipeline t(task) {
4743 let x: int = 42
4744 match x {
4745 true -> { log("bad") }
4746 42 -> { log("ok") }
4747 }
4748}"#,
4749 );
4750 let pattern_warns: Vec<_> = warns
4751 .iter()
4752 .filter(|w| w.contains("Match pattern type mismatch"))
4753 .collect();
4754 assert_eq!(pattern_warns.len(), 1);
4755 assert!(pattern_warns[0].contains("matching int against bool literal"));
4756 }
4757
4758 #[test]
4759 fn test_match_pattern_float_against_string() {
4760 let warns = warnings(
4761 r#"pipeline t(task) {
4762 let x: string = "hello"
4763 match x {
4764 3.14 -> { log("bad") }
4765 "hello" -> { log("ok") }
4766 }
4767}"#,
4768 );
4769 let pattern_warns: Vec<_> = warns
4770 .iter()
4771 .filter(|w| w.contains("Match pattern type mismatch"))
4772 .collect();
4773 assert_eq!(pattern_warns.len(), 1);
4774 assert!(pattern_warns[0].contains("matching string against float literal"));
4775 }
4776
4777 #[test]
4778 fn test_match_pattern_int_against_float_ok() {
4779 let warns = warnings(
4781 r#"pipeline t(task) {
4782 let x: float = 3.14
4783 match x {
4784 42 -> { log("ok") }
4785 _ -> { log("default") }
4786 }
4787}"#,
4788 );
4789 let pattern_warns: Vec<_> = warns
4790 .iter()
4791 .filter(|w| w.contains("Match pattern type mismatch"))
4792 .collect();
4793 assert!(pattern_warns.is_empty());
4794 }
4795
4796 #[test]
4797 fn test_match_pattern_float_against_int_ok() {
4798 let warns = warnings(
4800 r#"pipeline t(task) {
4801 let x: int = 42
4802 match x {
4803 3.14 -> { log("close") }
4804 _ -> { log("default") }
4805 }
4806}"#,
4807 );
4808 let pattern_warns: Vec<_> = warns
4809 .iter()
4810 .filter(|w| w.contains("Match pattern type mismatch"))
4811 .collect();
4812 assert!(pattern_warns.is_empty());
4813 }
4814
4815 #[test]
4816 fn test_match_pattern_correct_types_no_warning() {
4817 let warns = warnings(
4818 r#"pipeline t(task) {
4819 let x: int = 42
4820 match x {
4821 1 -> { log("one") }
4822 2 -> { log("two") }
4823 _ -> { log("other") }
4824 }
4825}"#,
4826 );
4827 let pattern_warns: Vec<_> = warns
4828 .iter()
4829 .filter(|w| w.contains("Match pattern type mismatch"))
4830 .collect();
4831 assert!(pattern_warns.is_empty());
4832 }
4833
4834 #[test]
4835 fn test_match_pattern_wildcard_no_warning() {
4836 let warns = warnings(
4837 r#"pipeline t(task) {
4838 let x: int = 42
4839 match x {
4840 _ -> { log("catch all") }
4841 }
4842}"#,
4843 );
4844 let pattern_warns: Vec<_> = warns
4845 .iter()
4846 .filter(|w| w.contains("Match pattern type mismatch"))
4847 .collect();
4848 assert!(pattern_warns.is_empty());
4849 }
4850
4851 #[test]
4852 fn test_match_pattern_untyped_no_warning() {
4853 let warns = warnings(
4855 r#"pipeline t(task) {
4856 let x = some_unknown_fn()
4857 match x {
4858 "hello" -> { log("string") }
4859 42 -> { log("int") }
4860 }
4861}"#,
4862 );
4863 let pattern_warns: Vec<_> = warns
4864 .iter()
4865 .filter(|w| w.contains("Match pattern type mismatch"))
4866 .collect();
4867 assert!(pattern_warns.is_empty());
4868 }
4869
4870 fn iface_errors(source: &str) -> Vec<String> {
4873 errors(source)
4874 .into_iter()
4875 .filter(|message| message.contains("does not satisfy interface"))
4876 .collect()
4877 }
4878
4879 #[test]
4880 fn test_interface_constraint_return_type_mismatch() {
4881 let warns = iface_errors(
4882 r#"pipeline t(task) {
4883 interface Sizable {
4884 fn size(self) -> int
4885 }
4886 struct Box { width: int }
4887 impl Box {
4888 fn size(self) -> string { return "nope" }
4889 }
4890 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4891 measure(Box({width: 3}))
4892}"#,
4893 );
4894 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4895 assert!(
4896 warns[0].contains("method 'size' returns 'string', expected 'int'"),
4897 "unexpected message: {}",
4898 warns[0]
4899 );
4900 }
4901
4902 #[test]
4903 fn test_interface_constraint_param_type_mismatch() {
4904 let warns = iface_errors(
4905 r#"pipeline t(task) {
4906 interface Processor {
4907 fn process(self, x: int) -> string
4908 }
4909 struct MyProc { name: string }
4910 impl MyProc {
4911 fn process(self, x: string) -> string { return x }
4912 }
4913 fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
4914 run_proc(MyProc({name: "a"}))
4915}"#,
4916 );
4917 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4918 assert!(
4919 warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
4920 "unexpected message: {}",
4921 warns[0]
4922 );
4923 }
4924
4925 #[test]
4926 fn test_interface_constraint_missing_method() {
4927 let warns = iface_errors(
4928 r#"pipeline t(task) {
4929 interface Sizable {
4930 fn size(self) -> int
4931 }
4932 struct Box { width: int }
4933 impl Box {
4934 fn area(self) -> int { return self.width }
4935 }
4936 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4937 measure(Box({width: 3}))
4938}"#,
4939 );
4940 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4941 assert!(
4942 warns[0].contains("missing method 'size'"),
4943 "unexpected message: {}",
4944 warns[0]
4945 );
4946 }
4947
4948 #[test]
4949 fn test_interface_constraint_param_count_mismatch() {
4950 let warns = iface_errors(
4951 r#"pipeline t(task) {
4952 interface Doubler {
4953 fn double(self, x: int) -> int
4954 }
4955 struct Bad { v: int }
4956 impl Bad {
4957 fn double(self) -> int { return self.v * 2 }
4958 }
4959 fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
4960 run_double(Bad({v: 5}))
4961}"#,
4962 );
4963 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
4964 assert!(
4965 warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
4966 "unexpected message: {}",
4967 warns[0]
4968 );
4969 }
4970
4971 #[test]
4972 fn test_interface_constraint_satisfied() {
4973 let warns = iface_errors(
4974 r#"pipeline t(task) {
4975 interface Sizable {
4976 fn size(self) -> int
4977 }
4978 struct Box { width: int, height: int }
4979 impl Box {
4980 fn size(self) -> int { return self.width * self.height }
4981 }
4982 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
4983 measure(Box({width: 3, height: 4}))
4984}"#,
4985 );
4986 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
4987 }
4988
4989 #[test]
4990 fn test_interface_constraint_untyped_impl_compatible() {
4991 let warns = iface_errors(
4993 r#"pipeline t(task) {
4994 interface Sizable {
4995 fn size(self) -> int
4996 }
4997 struct Box { width: int }
4998 impl Box {
4999 fn size(self) { return self.width }
5000 }
5001 fn measure<T>(item: T) where T: Sizable { log(item.size()) }
5002 measure(Box({width: 3}))
5003}"#,
5004 );
5005 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5006 }
5007
5008 #[test]
5009 fn test_interface_constraint_int_float_covariance() {
5010 let warns = iface_errors(
5012 r#"pipeline t(task) {
5013 interface Measurable {
5014 fn value(self) -> float
5015 }
5016 struct Gauge { v: int }
5017 impl Gauge {
5018 fn value(self) -> int { return self.v }
5019 }
5020 fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
5021 read_val(Gauge({v: 42}))
5022}"#,
5023 );
5024 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5025 }
5026
5027 #[test]
5028 fn test_interface_associated_type_constraint_satisfied() {
5029 let warns = iface_errors(
5030 r#"pipeline t(task) {
5031 interface Collection {
5032 type Item
5033 fn get(self, index: int) -> Item
5034 }
5035 struct Names {}
5036 impl Names {
5037 fn get(self, index: int) -> string { return "ada" }
5038 }
5039 fn first<C>(collection: C) where C: Collection {
5040 log(collection.get(0))
5041 }
5042 first(Names {})
5043}"#,
5044 );
5045 assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
5046 }
5047
5048 #[test]
5049 fn test_interface_associated_type_default_mismatch() {
5050 let warns = iface_errors(
5051 r#"pipeline t(task) {
5052 interface IntCollection {
5053 type Item = int
5054 fn get(self, index: int) -> Item
5055 }
5056 struct Labels {}
5057 impl Labels {
5058 fn get(self, index: int) -> string { return "oops" }
5059 }
5060 fn first<C>(collection: C) where C: IntCollection {
5061 log(collection.get(0))
5062 }
5063 first(Labels {})
5064}"#,
5065 );
5066 assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
5067 assert!(
5068 warns[0].contains("associated type 'Item' resolves to 'string', expected 'int'"),
5069 "unexpected message: {}",
5070 warns[0]
5071 );
5072 }
5073
5074 #[test]
5077 fn test_nil_narrowing_then_branch() {
5078 let errs = errors(
5080 r#"pipeline t(task) {
5081 fn greet(name: string | nil) {
5082 if name != nil {
5083 let s: string = name
5084 }
5085 }
5086}"#,
5087 );
5088 assert!(errs.is_empty(), "got: {:?}", errs);
5089 }
5090
5091 #[test]
5092 fn test_nil_narrowing_else_branch() {
5093 let errs = errors(
5095 r#"pipeline t(task) {
5096 fn check(x: string | nil) {
5097 if x != nil {
5098 let s: string = x
5099 } else {
5100 let n: nil = x
5101 }
5102 }
5103}"#,
5104 );
5105 assert!(errs.is_empty(), "got: {:?}", errs);
5106 }
5107
5108 #[test]
5109 fn test_nil_equality_narrows_both() {
5110 let errs = errors(
5112 r#"pipeline t(task) {
5113 fn check(x: string | nil) {
5114 if x == nil {
5115 let n: nil = x
5116 } else {
5117 let s: string = x
5118 }
5119 }
5120}"#,
5121 );
5122 assert!(errs.is_empty(), "got: {:?}", errs);
5123 }
5124
5125 #[test]
5126 fn test_truthiness_narrowing() {
5127 let errs = errors(
5129 r#"pipeline t(task) {
5130 fn check(x: string | nil) {
5131 if x {
5132 let s: string = x
5133 }
5134 }
5135}"#,
5136 );
5137 assert!(errs.is_empty(), "got: {:?}", errs);
5138 }
5139
5140 #[test]
5141 fn test_negation_narrowing() {
5142 let errs = errors(
5144 r#"pipeline t(task) {
5145 fn check(x: string | nil) {
5146 if !x {
5147 let n: nil = x
5148 } else {
5149 let s: string = x
5150 }
5151 }
5152}"#,
5153 );
5154 assert!(errs.is_empty(), "got: {:?}", errs);
5155 }
5156
5157 #[test]
5158 fn test_typeof_narrowing() {
5159 let errs = errors(
5161 r#"pipeline t(task) {
5162 fn check(x: string | int) {
5163 if type_of(x) == "string" {
5164 let s: string = x
5165 }
5166 }
5167}"#,
5168 );
5169 assert!(errs.is_empty(), "got: {:?}", errs);
5170 }
5171
5172 #[test]
5173 fn test_typeof_narrowing_else() {
5174 let errs = errors(
5176 r#"pipeline t(task) {
5177 fn check(x: string | int) {
5178 if type_of(x) == "string" {
5179 let s: string = x
5180 } else {
5181 let i: int = x
5182 }
5183 }
5184}"#,
5185 );
5186 assert!(errs.is_empty(), "got: {:?}", errs);
5187 }
5188
5189 #[test]
5190 fn test_typeof_neq_narrowing() {
5191 let errs = errors(
5193 r#"pipeline t(task) {
5194 fn check(x: string | int) {
5195 if type_of(x) != "string" {
5196 let i: int = x
5197 } else {
5198 let s: string = x
5199 }
5200 }
5201}"#,
5202 );
5203 assert!(errs.is_empty(), "got: {:?}", errs);
5204 }
5205
5206 #[test]
5207 fn test_and_combines_narrowing() {
5208 let errs = errors(
5210 r#"pipeline t(task) {
5211 fn check(x: string | int | nil) {
5212 if x != nil && type_of(x) == "string" {
5213 let s: string = x
5214 }
5215 }
5216}"#,
5217 );
5218 assert!(errs.is_empty(), "got: {:?}", errs);
5219 }
5220
5221 #[test]
5222 fn test_or_falsy_narrowing() {
5223 let errs = errors(
5225 r#"pipeline t(task) {
5226 fn check(x: string | nil, y: int | nil) {
5227 if x || y {
5228 // conservative: can't narrow
5229 } else {
5230 let xn: nil = x
5231 let yn: nil = y
5232 }
5233 }
5234}"#,
5235 );
5236 assert!(errs.is_empty(), "got: {:?}", errs);
5237 }
5238
5239 #[test]
5240 fn test_guard_narrows_outer_scope() {
5241 let errs = errors(
5242 r#"pipeline t(task) {
5243 fn check(x: string | nil) {
5244 guard x != nil else { return }
5245 let s: string = x
5246 }
5247}"#,
5248 );
5249 assert!(errs.is_empty(), "got: {:?}", errs);
5250 }
5251
5252 #[test]
5253 fn test_while_narrows_body() {
5254 let errs = errors(
5255 r#"pipeline t(task) {
5256 fn check(x: string | nil) {
5257 while x != nil {
5258 let s: string = x
5259 break
5260 }
5261 }
5262}"#,
5263 );
5264 assert!(errs.is_empty(), "got: {:?}", errs);
5265 }
5266
5267 #[test]
5268 fn test_early_return_narrows_after_if() {
5269 let errs = errors(
5271 r#"pipeline t(task) {
5272 fn check(x: string | nil) -> string {
5273 if x == nil {
5274 return "default"
5275 }
5276 let s: string = x
5277 return s
5278 }
5279}"#,
5280 );
5281 assert!(errs.is_empty(), "got: {:?}", errs);
5282 }
5283
5284 #[test]
5285 fn test_early_throw_narrows_after_if() {
5286 let errs = errors(
5287 r#"pipeline t(task) {
5288 fn check(x: string | nil) {
5289 if x == nil {
5290 throw "missing"
5291 }
5292 let s: string = x
5293 }
5294}"#,
5295 );
5296 assert!(errs.is_empty(), "got: {:?}", errs);
5297 }
5298
5299 #[test]
5300 fn test_no_narrowing_unknown_type() {
5301 let errs = errors(
5303 r#"pipeline t(task) {
5304 fn check(x) {
5305 if x != nil {
5306 let s: string = x
5307 }
5308 }
5309}"#,
5310 );
5311 assert!(errs.is_empty(), "got: {:?}", errs);
5314 }
5315
5316 #[test]
5317 fn test_reassignment_invalidates_narrowing() {
5318 let errs = errors(
5320 r#"pipeline t(task) {
5321 fn check(x: string | nil) {
5322 var y: string | nil = x
5323 if y != nil {
5324 let s: string = y
5325 y = nil
5326 let s2: string = y
5327 }
5328 }
5329}"#,
5330 );
5331 assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
5333 assert!(
5334 errs[0].contains("Type mismatch"),
5335 "expected type mismatch, got: {}",
5336 errs[0]
5337 );
5338 }
5339
5340 #[test]
5341 fn test_let_immutable_warning() {
5342 let all = check_source(
5343 r#"pipeline t(task) {
5344 let x = 42
5345 x = 43
5346}"#,
5347 );
5348 let warnings: Vec<_> = all
5349 .iter()
5350 .filter(|d| d.severity == DiagnosticSeverity::Warning)
5351 .collect();
5352 assert!(
5353 warnings.iter().any(|w| w.message.contains("immutable")),
5354 "expected immutability warning, got: {:?}",
5355 warnings
5356 );
5357 }
5358
5359 #[test]
5360 fn test_nested_narrowing() {
5361 let errs = errors(
5362 r#"pipeline t(task) {
5363 fn check(x: string | int | nil) {
5364 if x != nil {
5365 if type_of(x) == "int" {
5366 let i: int = x
5367 }
5368 }
5369 }
5370}"#,
5371 );
5372 assert!(errs.is_empty(), "got: {:?}", errs);
5373 }
5374
5375 #[test]
5376 fn test_match_narrows_arms() {
5377 let errs = errors(
5378 r#"pipeline t(task) {
5379 fn check(x: string | int) {
5380 match x {
5381 "hello" -> {
5382 let s: string = x
5383 }
5384 42 -> {
5385 let i: int = x
5386 }
5387 _ -> {}
5388 }
5389 }
5390}"#,
5391 );
5392 assert!(errs.is_empty(), "got: {:?}", errs);
5393 }
5394
5395 #[test]
5396 fn test_has_narrows_optional_field() {
5397 let errs = errors(
5398 r#"pipeline t(task) {
5399 fn check(x: {name?: string, age: int}) {
5400 if x.has("name") {
5401 let n: {name: string, age: int} = x
5402 }
5403 }
5404}"#,
5405 );
5406 assert!(errs.is_empty(), "got: {:?}", errs);
5407 }
5408
5409 fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
5414 let mut lexer = Lexer::new(source);
5415 let tokens = lexer.tokenize().unwrap();
5416 let mut parser = Parser::new(tokens);
5417 let program = parser.parse().unwrap();
5418 TypeChecker::new().check_with_source(&program, source)
5419 }
5420
5421 #[test]
5422 fn test_fix_string_plus_int_literal() {
5423 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
5424 let diags = check_source_with_source(source);
5425 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5426 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5427 let fix = fixable[0].fix.as_ref().unwrap();
5428 assert_eq!(fix.len(), 1);
5429 assert_eq!(fix[0].replacement, "\"hello ${42}\"");
5430 }
5431
5432 #[test]
5433 fn test_fix_int_plus_string_literal() {
5434 let source = "pipeline t(task) {\n let x = 42 + \"hello\"\n log(x)\n}";
5435 let diags = check_source_with_source(source);
5436 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5437 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5438 let fix = fixable[0].fix.as_ref().unwrap();
5439 assert_eq!(fix[0].replacement, "\"${42}hello\"");
5440 }
5441
5442 #[test]
5443 fn test_fix_string_plus_variable() {
5444 let source = "pipeline t(task) {\n let n: int = 5\n let x = \"count: \" + n\n log(x)\n}";
5445 let diags = check_source_with_source(source);
5446 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5447 assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
5448 let fix = fixable[0].fix.as_ref().unwrap();
5449 assert_eq!(fix[0].replacement, "\"count: ${n}\"");
5450 }
5451
5452 #[test]
5453 fn test_no_fix_int_plus_int() {
5454 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}";
5456 let diags = check_source_with_source(source);
5457 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5458 assert!(
5459 fixable.is_empty(),
5460 "no fix expected for numeric ops, got: {fixable:?}"
5461 );
5462 }
5463
5464 #[test]
5465 fn test_no_fix_without_source() {
5466 let source = "pipeline t(task) {\n let x = \"hello \" + 42\n log(x)\n}";
5467 let diags = check_source(source);
5468 let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
5469 assert!(
5470 fixable.is_empty(),
5471 "without source, no fix should be generated"
5472 );
5473 }
5474
5475 #[test]
5478 fn test_union_exhaustive_match_no_warning() {
5479 let warns = warnings(
5480 r#"pipeline t(task) {
5481 let x: string | int | nil = nil
5482 match x {
5483 "hello" -> { log("s") }
5484 42 -> { log("i") }
5485 nil -> { log("n") }
5486 }
5487}"#,
5488 );
5489 let union_warns: Vec<_> = warns
5490 .iter()
5491 .filter(|w| w.contains("Non-exhaustive match on union"))
5492 .collect();
5493 assert!(union_warns.is_empty());
5494 }
5495
5496 #[test]
5497 fn test_union_non_exhaustive_match_warning() {
5498 let warns = warnings(
5499 r#"pipeline t(task) {
5500 let x: string | int | nil = nil
5501 match x {
5502 "hello" -> { log("s") }
5503 42 -> { log("i") }
5504 }
5505}"#,
5506 );
5507 let union_warns: Vec<_> = warns
5508 .iter()
5509 .filter(|w| w.contains("Non-exhaustive match on union"))
5510 .collect();
5511 assert_eq!(union_warns.len(), 1);
5512 assert!(union_warns[0].contains("nil"));
5513 }
5514
5515 #[test]
5518 fn test_nil_coalesce_non_union_preserves_left_type() {
5519 let errs = errors(
5521 r#"pipeline t(task) {
5522 let x: int = 42
5523 let y: int = x ?? 0
5524}"#,
5525 );
5526 assert!(errs.is_empty());
5527 }
5528
5529 #[test]
5530 fn test_nil_coalesce_nil_returns_right_type() {
5531 let errs = errors(
5532 r#"pipeline t(task) {
5533 let x: string = nil ?? "fallback"
5534}"#,
5535 );
5536 assert!(errs.is_empty());
5537 }
5538
5539 #[test]
5542 fn test_never_is_subtype_of_everything() {
5543 let tc = TypeChecker::new();
5544 let scope = TypeScope::new();
5545 assert!(tc.types_compatible(&TypeExpr::Named("string".into()), &TypeExpr::Never, &scope));
5546 assert!(tc.types_compatible(&TypeExpr::Named("int".into()), &TypeExpr::Never, &scope));
5547 assert!(tc.types_compatible(
5548 &TypeExpr::Union(vec![
5549 TypeExpr::Named("string".into()),
5550 TypeExpr::Named("nil".into()),
5551 ]),
5552 &TypeExpr::Never,
5553 &scope,
5554 ));
5555 }
5556
5557 #[test]
5558 fn test_nothing_is_subtype_of_never() {
5559 let tc = TypeChecker::new();
5560 let scope = TypeScope::new();
5561 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("string".into()), &scope));
5562 assert!(!tc.types_compatible(&TypeExpr::Never, &TypeExpr::Named("int".into()), &scope));
5563 }
5564
5565 #[test]
5566 fn test_never_never_compatible() {
5567 let tc = TypeChecker::new();
5568 let scope = TypeScope::new();
5569 assert!(tc.types_compatible(&TypeExpr::Never, &TypeExpr::Never, &scope));
5570 }
5571
5572 #[test]
5573 fn test_simplify_union_removes_never() {
5574 assert_eq!(
5575 simplify_union(vec![TypeExpr::Never, TypeExpr::Named("string".into())]),
5576 TypeExpr::Named("string".into()),
5577 );
5578 assert_eq!(
5579 simplify_union(vec![TypeExpr::Never, TypeExpr::Never]),
5580 TypeExpr::Never,
5581 );
5582 assert_eq!(
5583 simplify_union(vec![
5584 TypeExpr::Named("string".into()),
5585 TypeExpr::Never,
5586 TypeExpr::Named("int".into()),
5587 ]),
5588 TypeExpr::Union(vec![
5589 TypeExpr::Named("string".into()),
5590 TypeExpr::Named("int".into()),
5591 ]),
5592 );
5593 }
5594
5595 #[test]
5596 fn test_remove_from_union_exhausted_returns_never() {
5597 let result = remove_from_union(&[TypeExpr::Named("string".into())], "string");
5598 assert_eq!(result, Some(TypeExpr::Never));
5599 }
5600
5601 #[test]
5602 fn test_if_else_one_branch_throws_infers_other() {
5603 let errs = errors(
5605 r#"pipeline t(task) {
5606 fn foo(x: bool) -> int {
5607 let result: int = if x { 42 } else { throw "err" }
5608 return result
5609 }
5610}"#,
5611 );
5612 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5613 }
5614
5615 #[test]
5616 fn test_if_else_both_branches_throw_infers_never() {
5617 let errs = errors(
5619 r#"pipeline t(task) {
5620 fn foo(x: bool) -> string {
5621 let result: string = if x { throw "a" } else { throw "b" }
5622 return result
5623 }
5624}"#,
5625 );
5626 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5627 }
5628
5629 #[test]
5632 fn test_unreachable_after_return() {
5633 let warns = warnings(
5634 r#"pipeline t(task) {
5635 fn foo() -> int {
5636 return 1
5637 let x = 2
5638 }
5639}"#,
5640 );
5641 assert!(
5642 warns.iter().any(|w| w.contains("unreachable")),
5643 "expected unreachable warning: {warns:?}"
5644 );
5645 }
5646
5647 #[test]
5648 fn test_unreachable_after_throw() {
5649 let warns = warnings(
5650 r#"pipeline t(task) {
5651 fn foo() {
5652 throw "err"
5653 let x = 2
5654 }
5655}"#,
5656 );
5657 assert!(
5658 warns.iter().any(|w| w.contains("unreachable")),
5659 "expected unreachable warning: {warns:?}"
5660 );
5661 }
5662
5663 #[test]
5664 fn test_unreachable_after_composite_exit() {
5665 let warns = warnings(
5666 r#"pipeline t(task) {
5667 fn foo(x: bool) {
5668 if x { return 1 } else { throw "err" }
5669 let y = 2
5670 }
5671}"#,
5672 );
5673 assert!(
5674 warns.iter().any(|w| w.contains("unreachable")),
5675 "expected unreachable warning: {warns:?}"
5676 );
5677 }
5678
5679 #[test]
5680 fn test_no_unreachable_warning_when_reachable() {
5681 let warns = warnings(
5682 r#"pipeline t(task) {
5683 fn foo(x: bool) {
5684 if x { return 1 }
5685 let y = 2
5686 }
5687}"#,
5688 );
5689 assert!(
5690 !warns.iter().any(|w| w.contains("unreachable")),
5691 "unexpected unreachable warning: {warns:?}"
5692 );
5693 }
5694
5695 #[test]
5698 fn test_catch_typed_error_variable() {
5699 let errs = errors(
5701 r#"pipeline t(task) {
5702 enum AppError { NotFound, Timeout }
5703 try {
5704 throw AppError.NotFound
5705 } catch (e: AppError) {
5706 let x: AppError = e
5707 }
5708}"#,
5709 );
5710 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5711 }
5712
5713 #[test]
5716 fn test_unreachable_with_never_arg_no_error() {
5717 let errs = errors(
5719 r#"pipeline t(task) {
5720 fn foo(x: string | int) {
5721 if type_of(x) == "string" { return }
5722 if type_of(x) == "int" { return }
5723 unreachable(x)
5724 }
5725}"#,
5726 );
5727 assert!(
5728 !errs.iter().any(|e| e.contains("unreachable")),
5729 "unexpected unreachable error: {errs:?}"
5730 );
5731 }
5732
5733 #[test]
5734 fn test_unreachable_with_remaining_types_errors() {
5735 let errs = errors(
5737 r#"pipeline t(task) {
5738 fn foo(x: string | int | nil) {
5739 if type_of(x) == "string" { return }
5740 unreachable(x)
5741 }
5742}"#,
5743 );
5744 assert!(
5745 errs.iter()
5746 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
5747 "expected unreachable error about remaining types: {errs:?}"
5748 );
5749 }
5750
5751 #[test]
5752 fn test_unreachable_no_args_no_compile_error() {
5753 let errs = errors(
5754 r#"pipeline t(task) {
5755 fn foo() {
5756 unreachable()
5757 }
5758}"#,
5759 );
5760 assert!(
5761 !errs
5762 .iter()
5763 .any(|e| e.contains("unreachable") && e.contains("not all cases")),
5764 "unreachable() with no args should not produce type error: {errs:?}"
5765 );
5766 }
5767
5768 #[test]
5769 fn test_never_type_annotation_parses() {
5770 let errs = errors(
5771 r#"pipeline t(task) {
5772 fn foo() -> never {
5773 throw "always throws"
5774 }
5775}"#,
5776 );
5777 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
5778 }
5779
5780 #[test]
5781 fn test_format_type_never() {
5782 assert_eq!(format_type(&TypeExpr::Never), "never");
5783 }
5784
5785 fn check_source_strict(source: &str) -> Vec<TypeDiagnostic> {
5788 let mut lexer = Lexer::new(source);
5789 let tokens = lexer.tokenize().unwrap();
5790 let mut parser = Parser::new(tokens);
5791 let program = parser.parse().unwrap();
5792 TypeChecker::with_strict_types(true).check(&program)
5793 }
5794
5795 fn strict_warnings(source: &str) -> Vec<String> {
5796 check_source_strict(source)
5797 .into_iter()
5798 .filter(|d| d.severity == DiagnosticSeverity::Warning)
5799 .map(|d| d.message)
5800 .collect()
5801 }
5802
5803 #[test]
5804 fn test_strict_types_json_parse_property_access() {
5805 let warns = strict_warnings(
5806 r#"pipeline t(task) {
5807 let data = json_parse("{}")
5808 log(data.name)
5809}"#,
5810 );
5811 assert!(
5812 warns.iter().any(|w| w.contains("unvalidated")),
5813 "expected unvalidated warning, got: {warns:?}"
5814 );
5815 }
5816
5817 #[test]
5818 fn test_strict_types_direct_chain_access() {
5819 let warns = strict_warnings(
5820 r#"pipeline t(task) {
5821 log(json_parse("{}").name)
5822}"#,
5823 );
5824 assert!(
5825 warns.iter().any(|w| w.contains("Direct property access")),
5826 "expected direct access warning, got: {warns:?}"
5827 );
5828 }
5829
5830 #[test]
5831 fn test_strict_types_schema_expect_clears() {
5832 let warns = strict_warnings(
5833 r#"pipeline t(task) {
5834 let my_schema = {type: "object", properties: {name: {type: "string"}}}
5835 let data = json_parse("{}")
5836 schema_expect(data, my_schema)
5837 log(data.name)
5838}"#,
5839 );
5840 assert!(
5841 !warns.iter().any(|w| w.contains("unvalidated")),
5842 "expected no unvalidated warning after schema_expect, got: {warns:?}"
5843 );
5844 }
5845
5846 #[test]
5847 fn test_strict_types_schema_is_if_guard() {
5848 let warns = strict_warnings(
5849 r#"pipeline t(task) {
5850 let my_schema = {type: "object", properties: {name: {type: "string"}}}
5851 let data = json_parse("{}")
5852 if schema_is(data, my_schema) {
5853 log(data.name)
5854 }
5855}"#,
5856 );
5857 assert!(
5858 !warns.iter().any(|w| w.contains("unvalidated")),
5859 "expected no unvalidated warning inside schema_is guard, got: {warns:?}"
5860 );
5861 }
5862
5863 #[test]
5864 fn test_strict_types_shape_annotation_clears() {
5865 let warns = strict_warnings(
5866 r#"pipeline t(task) {
5867 let data: {name: string, age: int} = json_parse("{}")
5868 log(data.name)
5869}"#,
5870 );
5871 assert!(
5872 !warns.iter().any(|w| w.contains("unvalidated")),
5873 "expected no warning with shape annotation, got: {warns:?}"
5874 );
5875 }
5876
5877 #[test]
5878 fn test_strict_types_propagation() {
5879 let warns = strict_warnings(
5880 r#"pipeline t(task) {
5881 let data = json_parse("{}")
5882 let x = data
5883 log(x.name)
5884}"#,
5885 );
5886 assert!(
5887 warns
5888 .iter()
5889 .any(|w| w.contains("unvalidated") && w.contains("'x'")),
5890 "expected propagation warning for x, got: {warns:?}"
5891 );
5892 }
5893
5894 #[test]
5895 fn test_strict_types_non_boundary_no_warning() {
5896 let warns = strict_warnings(
5897 r#"pipeline t(task) {
5898 let x = len("hello")
5899 log(x)
5900}"#,
5901 );
5902 assert!(
5903 !warns.iter().any(|w| w.contains("unvalidated")),
5904 "non-boundary function should not be flagged, got: {warns:?}"
5905 );
5906 }
5907
5908 #[test]
5909 fn test_strict_types_subscript_access() {
5910 let warns = strict_warnings(
5911 r#"pipeline t(task) {
5912 let data = json_parse("{}")
5913 log(data["name"])
5914}"#,
5915 );
5916 assert!(
5917 warns.iter().any(|w| w.contains("unvalidated")),
5918 "expected subscript warning, got: {warns:?}"
5919 );
5920 }
5921
5922 #[test]
5923 fn test_strict_types_disabled_by_default() {
5924 let diags = check_source(
5925 r#"pipeline t(task) {
5926 let data = json_parse("{}")
5927 log(data.name)
5928}"#,
5929 );
5930 assert!(
5931 !diags.iter().any(|d| d.message.contains("unvalidated")),
5932 "strict types should be off by default, got: {diags:?}"
5933 );
5934 }
5935
5936 #[test]
5937 fn test_strict_types_llm_call_without_schema() {
5938 let warns = strict_warnings(
5939 r#"pipeline t(task) {
5940 let result = llm_call("prompt", "system")
5941 log(result.text)
5942}"#,
5943 );
5944 assert!(
5945 warns.iter().any(|w| w.contains("unvalidated")),
5946 "llm_call without schema should warn, got: {warns:?}"
5947 );
5948 }
5949
5950 #[test]
5951 fn test_strict_types_llm_call_with_schema_clean() {
5952 let warns = strict_warnings(
5953 r#"pipeline t(task) {
5954 let result = llm_call("prompt", "system", {
5955 schema: {type: "object", properties: {name: {type: "string"}}}
5956 })
5957 log(result.data)
5958 log(result.text)
5959}"#,
5960 );
5961 assert!(
5962 !warns.iter().any(|w| w.contains("unvalidated")),
5963 "llm_call with schema should not warn, got: {warns:?}"
5964 );
5965 }
5966
5967 #[test]
5968 fn test_strict_types_schema_expect_result_typed() {
5969 let warns = strict_warnings(
5970 r#"pipeline t(task) {
5971 let my_schema = {type: "object", properties: {name: {type: "string"}}}
5972 let validated = schema_expect(json_parse("{}"), my_schema)
5973 log(validated.name)
5974}"#,
5975 );
5976 assert!(
5977 !warns.iter().any(|w| w.contains("unvalidated")),
5978 "schema_expect result should be typed, got: {warns:?}"
5979 );
5980 }
5981
5982 #[test]
5983 fn test_strict_types_realistic_orchestration() {
5984 let warns = strict_warnings(
5985 r#"pipeline t(task) {
5986 let payload_schema = {type: "object", properties: {
5987 name: {type: "string"},
5988 steps: {type: "list", items: {type: "string"}}
5989 }}
5990
5991 // Good: schema-aware llm_call
5992 let result = llm_call("generate a workflow", "system", {
5993 schema: payload_schema
5994 })
5995 let workflow_name = result.data.name
5996
5997 // Good: validate then access
5998 let raw = json_parse("{}")
5999 schema_expect(raw, payload_schema)
6000 let steps = raw.steps
6001
6002 log(workflow_name)
6003 log(steps)
6004}"#,
6005 );
6006 assert!(
6007 !warns.iter().any(|w| w.contains("unvalidated")),
6008 "validated orchestration should be clean, got: {warns:?}"
6009 );
6010 }
6011
6012 #[test]
6013 fn test_strict_types_llm_call_with_schema_via_variable() {
6014 let warns = strict_warnings(
6015 r#"pipeline t(task) {
6016 let my_schema = {type: "object", properties: {score: {type: "float"}}}
6017 let result = llm_call("rate this", "system", {
6018 schema: my_schema
6019 })
6020 log(result.data.score)
6021}"#,
6022 );
6023 assert!(
6024 !warns.iter().any(|w| w.contains("unvalidated")),
6025 "llm_call with schema variable should not warn, got: {warns:?}"
6026 );
6027 }
6028}