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