1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::rc::Rc;
3
4use crate::ast::*;
5use crate::builtin_signatures;
6use crate::diagnostic_codes::{Code, Repair};
7use harn_lexer::{FixEdit, Span};
8
9type TypeMismatchEvidence = (Option<(Span, String)>, Option<Span>);
10
11mod binary_ops;
12mod exits;
13mod format;
14mod inference;
15pub mod method_registry;
16mod schema_inference;
17mod scope;
18mod union;
19
20pub use exits::{block_definitely_exits, stmt_definitely_exits};
21pub use format::{format_type, shape_mismatch_detail};
22
23pub fn substitute_type_expr(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
27 TypeChecker::apply_type_bindings(ty, bindings)
28}
29
30use schema_inference::output_schema_type_expr_from_node;
31use scope::TypeScope;
32
33#[derive(Debug, Clone)]
35pub struct InlayHintInfo {
36 pub line: usize,
38 pub column: usize,
39 pub label: String,
41}
42
43#[derive(Debug, Clone)]
49pub struct BindingTypeInfo {
50 pub name: String,
51 pub span: Span,
52 pub type_expr: TypeExpr,
53}
54
55#[derive(Debug, Clone)]
57pub struct TypeCheckFacts {
58 pub diagnostics: Vec<TypeDiagnostic>,
59 pub inlay_hints: Vec<InlayHintInfo>,
60 pub binding_types: Vec<BindingTypeInfo>,
61}
62
63#[derive(Debug, Clone)]
65pub struct NamespaceImportBinding {
66 pub module_path: String,
68 pub members: BTreeSet<String>,
70 pub member_types: std::collections::BTreeMap<String, TypeExpr>,
81 pub member_param_names: std::collections::BTreeMap<String, Vec<String>>,
85 pub member_required_params: std::collections::BTreeMap<String, usize>,
88}
89
90#[derive(Debug, Clone)]
92pub struct TypeDiagnostic {
93 pub code: Code,
94 pub message: String,
95 pub severity: DiagnosticSeverity,
96 pub span: Option<Span>,
97 pub help: Option<String>,
98 pub related: Vec<RelatedDiagnostic>,
99 pub fix: Option<Vec<FixEdit>>,
101 pub details: Option<DiagnosticDetails>,
106 pub repair: Option<Repair>,
112}
113
114#[derive(Debug, Clone)]
115pub struct RelatedDiagnostic {
116 pub span: Span,
117 pub message: String,
118}
119
120#[derive(Debug, Clone)]
127pub enum DiagnosticDetails {
128 TypeMismatch {
131 expected: TypeExpr,
132 actual: TypeExpr,
133 },
134 UnresolvedName { name: String },
138 CallArity {
142 callee: String,
143 parameter_types: Vec<Option<TypeExpr>>,
144 required: usize,
145 actual: usize,
146 },
147 FlowCapabilityBoundary {
151 parameter: String,
152 capabilities: Vec<String>,
153 allowed: Vec<String>,
154 },
155 NonExhaustiveMatch { missing: Vec<String> },
162 LintRule { rule: &'static str },
167 ImplicitAnyParameter { owner: String, parameter: String },
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum DiagnosticSeverity {
175 Error,
176 Warning,
177}
178
179pub struct TypeChecker {
181 diagnostics: Vec<TypeDiagnostic>,
182 scope: Rc<TypeScope>,
188 source: Option<String>,
189 hints: Vec<InlayHintInfo>,
190 binding_types: Vec<BindingTypeInfo>,
191 strict_types: bool,
193 legacy_ambient_capabilities: bool,
197 privileged_wire_builtins: bool,
200 fn_depth: usize,
203 stream_fn_depth: usize,
205 stream_emit_types: Vec<Option<TypeExpr>>,
207 expected_return_types: Vec<Option<TypeExpr>>,
211 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
216 imported_names: Option<HashSet<String>>,
223 imported_type_decls: Vec<SNode>,
227 imported_callable_decls: Vec<SNode>,
230 namespace_imports: std::collections::HashMap<String, NamespaceImportBinding>,
233 const_env: crate::const_eval::ConstEnv,
237 subtype_cycle_guard: std::cell::RefCell<Vec<(TypeExpr, TypeExpr)>>,
245}
246
247impl TypeChecker {
248 pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
249 TypeExpr::Named("_".into())
250 }
251
252 pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
253 matches!(ty, TypeExpr::Named(name) if name == "_")
254 }
255
256 pub(in crate::typechecker) fn contains_wildcard_type(ty: &TypeExpr) -> bool {
257 match ty {
258 TypeExpr::Named(name) => name == "_",
259 TypeExpr::Union(members) | TypeExpr::Intersection(members) => {
260 members.iter().any(Self::contains_wildcard_type)
261 }
262 TypeExpr::Tuple(items) => items.iter().any(Self::contains_wildcard_type),
263 TypeExpr::Shape(fields) => fields
264 .iter()
265 .any(|field| Self::contains_wildcard_type(&field.type_expr)),
266 TypeExpr::OpenShape { fields, rests } => {
267 fields
268 .iter()
269 .any(|field| Self::contains_wildcard_type(&field.type_expr))
270 || rests.iter().any(Self::contains_wildcard_type)
271 }
272 TypeExpr::List(inner)
273 | TypeExpr::Iter(inner)
274 | TypeExpr::Generator(inner)
275 | TypeExpr::Stream(inner)
276 | TypeExpr::Owned(inner) => Self::contains_wildcard_type(inner),
277 TypeExpr::DictType(key, value) => {
278 Self::contains_wildcard_type(key) || Self::contains_wildcard_type(value)
279 }
280 TypeExpr::Applied { args, .. } => args.iter().any(Self::contains_wildcard_type),
281 TypeExpr::FnType {
282 params,
283 return_type,
284 } => {
285 params.iter().any(Self::contains_wildcard_type)
286 || Self::contains_wildcard_type(return_type)
287 }
288 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
289 }
290 }
291
292 pub(in crate::typechecker) fn contains_type_param(
293 ty: &TypeExpr,
294 type_params: &BTreeSet<String>,
295 ) -> bool {
296 match ty {
297 TypeExpr::Named(name) => type_params.contains(name),
298 TypeExpr::Union(members) | TypeExpr::Intersection(members) => members
299 .iter()
300 .any(|member| Self::contains_type_param(member, type_params)),
301 TypeExpr::Tuple(items) => items
302 .iter()
303 .any(|item| Self::contains_type_param(item, type_params)),
304 TypeExpr::Shape(fields) => fields
305 .iter()
306 .any(|field| Self::contains_type_param(&field.type_expr, type_params)),
307 TypeExpr::OpenShape { fields, rests } => {
308 fields
309 .iter()
310 .any(|field| Self::contains_type_param(&field.type_expr, type_params))
311 || rests
312 .iter()
313 .any(|rest| Self::contains_type_param(rest, type_params))
314 }
315 TypeExpr::List(inner)
316 | TypeExpr::Iter(inner)
317 | TypeExpr::Generator(inner)
318 | TypeExpr::Stream(inner)
319 | TypeExpr::Owned(inner) => Self::contains_type_param(inner, type_params),
320 TypeExpr::DictType(key, value) => {
321 Self::contains_type_param(key, type_params)
322 || Self::contains_type_param(value, type_params)
323 }
324 TypeExpr::Applied { args, .. } => args
325 .iter()
326 .any(|arg| Self::contains_type_param(arg, type_params)),
327 TypeExpr::FnType {
328 params,
329 return_type,
330 } => {
331 params
332 .iter()
333 .any(|param| Self::contains_type_param(param, type_params))
334 || Self::contains_type_param(return_type, type_params)
335 }
336 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
337 }
338 }
339
340 pub(in crate::typechecker) fn contains_abstract_type(
341 &self,
342 ty: &TypeExpr,
343 scope: &TypeScope,
344 ) -> bool {
345 match ty {
346 TypeExpr::Named(name) => {
347 matches!(name.as_str(), "_" | "any" | "unknown")
348 || scope.is_generic_type_param(name)
349 }
350 TypeExpr::Union(members) | TypeExpr::Intersection(members) => members
351 .iter()
352 .any(|member| self.contains_abstract_type(member, scope)),
353 TypeExpr::Tuple(items) => items
354 .iter()
355 .any(|item| self.contains_abstract_type(item, scope)),
356 TypeExpr::Shape(fields) => fields
357 .iter()
358 .any(|field| self.contains_abstract_type(&field.type_expr, scope)),
359 TypeExpr::OpenShape { fields, rests } => {
360 fields
361 .iter()
362 .any(|field| self.contains_abstract_type(&field.type_expr, scope))
363 || rests
364 .iter()
365 .any(|rest| self.contains_abstract_type(rest, scope))
366 }
367 TypeExpr::List(inner)
368 | TypeExpr::Iter(inner)
369 | TypeExpr::Generator(inner)
370 | TypeExpr::Stream(inner)
371 | TypeExpr::Owned(inner) => self.contains_abstract_type(inner, scope),
372 TypeExpr::DictType(key, value) => {
373 self.contains_abstract_type(key, scope) || self.contains_abstract_type(value, scope)
374 }
375 TypeExpr::Applied { args, .. } => args
376 .iter()
377 .any(|arg| self.contains_abstract_type(arg, scope)),
378 TypeExpr::FnType {
379 params,
380 return_type,
381 } => {
382 params
383 .iter()
384 .any(|param| self.contains_abstract_type(param, scope))
385 || self.contains_abstract_type(return_type, scope)
386 }
387 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
388 }
389 }
390
391 pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
392 match ty {
393 TypeExpr::Named(name) => Some(name.as_str()),
394 TypeExpr::Applied { name, .. } => Some(name.as_str()),
395 _ => None,
396 }
397 }
398
399 pub fn new() -> Self {
400 Self {
401 diagnostics: Vec::new(),
402 scope: Rc::new(TypeScope::new()),
403 source: None,
404 hints: Vec::new(),
405 binding_types: Vec::new(),
406 strict_types: false,
407 legacy_ambient_capabilities: crate::legacy_ambient_capabilities_enabled(),
408 privileged_wire_builtins: false,
409 fn_depth: 0,
410 stream_fn_depth: 0,
411 stream_emit_types: Vec::new(),
412 expected_return_types: Vec::new(),
413 deprecated_fns: std::collections::HashMap::new(),
414 imported_names: None,
415 imported_type_decls: Vec::new(),
416 imported_callable_decls: Vec::new(),
417 namespace_imports: std::collections::HashMap::new(),
418 const_env: crate::const_eval::ConstEnv::new(),
419 subtype_cycle_guard: std::cell::RefCell::new(Vec::new()),
420 }
421 }
422
423 pub fn with_strict_types(strict: bool) -> Self {
426 Self {
427 diagnostics: Vec::new(),
428 scope: Rc::new(TypeScope::new()),
429 source: None,
430 hints: Vec::new(),
431 binding_types: Vec::new(),
432 strict_types: strict,
433 legacy_ambient_capabilities: crate::legacy_ambient_capabilities_enabled(),
434 privileged_wire_builtins: false,
435 fn_depth: 0,
436 stream_fn_depth: 0,
437 stream_emit_types: Vec::new(),
438 expected_return_types: Vec::new(),
439 deprecated_fns: std::collections::HashMap::new(),
440 imported_names: None,
441 imported_type_decls: Vec::new(),
442 imported_callable_decls: Vec::new(),
443 namespace_imports: std::collections::HashMap::new(),
444 const_env: crate::const_eval::ConstEnv::new(),
445 subtype_cycle_guard: std::cell::RefCell::new(Vec::new()),
446 }
447 }
448
449 pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
460 self.imported_names = Some(imported);
461 self
462 }
463
464 #[cfg(test)]
465 pub(crate) fn with_legacy_ambient_capabilities(mut self) -> Self {
466 self.legacy_ambient_capabilities = true;
467 self
468 }
469
470 pub fn with_privileged_wire_builtins(mut self, enabled: bool) -> Self {
471 self.privileged_wire_builtins = enabled;
472 self
473 }
474
475 pub(in crate::typechecker) fn lookup_builtin(
476 &self,
477 name: &str,
478 ) -> Option<&'static crate::builtin_signatures::BuiltinSignature> {
479 crate::builtin_signatures::lookup_with_privileged_wire(name, self.privileged_wire_builtins)
480 }
481
482 pub(in crate::typechecker) fn is_builtin(&self, name: &str) -> bool {
483 crate::builtin_signatures::is_builtin_with_privileged_wire(
484 name,
485 self.privileged_wire_builtins,
486 )
487 }
488
489 pub fn with_imported_type_decls(mut self, imported: Vec<SNode>) -> Self {
493 self.imported_type_decls = imported;
494 self
495 }
496
497 pub fn with_imported_callable_decls(mut self, imported: Vec<SNode>) -> Self {
502 self.imported_callable_decls = imported;
503 self
504 }
505
506 pub fn with_namespace_imports(
512 mut self,
513 imports: impl IntoIterator<Item = (String, NamespaceImportBinding)>,
514 ) -> Self {
515 let imports: std::collections::HashMap<String, NamespaceImportBinding> =
516 imports.into_iter().collect();
517 if let Some(names) = self.imported_names.as_mut() {
518 for alias in imports.keys() {
519 names.insert(alias.clone());
520 }
521 }
522 self.namespace_imports = imports;
523 self
524 }
525
526 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
528 self.source = Some(source.to_string());
529 self.check_inner(program).diagnostics
530 }
531
532 pub fn check_strict_with_source(
534 mut self,
535 program: &[SNode],
536 source: &str,
537 ) -> Vec<TypeDiagnostic> {
538 self.source = Some(source.to_string());
539 self.strict_types = true;
540 self.check_inner(program).diagnostics
541 }
542
543 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
545 self.check_inner(program).diagnostics
546 }
547
548 pub(in crate::typechecker) fn detect_boundary_source(
552 value: &SNode,
553 scope: &TypeScope,
554 ) -> Option<String> {
555 match &value.node {
556 Node::FunctionCall { name, args, .. } => {
557 if !builtin_signatures::is_untyped_boundary_source(name) {
558 return None;
559 }
560 if (name == "llm_call" || name == "llm_completion")
562 && Self::llm_call_has_typed_schema_option(args, scope)
563 {
564 return None;
565 }
566 Some(name.clone())
567 }
568 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
569 _ => None,
570 }
571 }
572
573 pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
579 args: &[SNode],
580 scope: &TypeScope,
581 ) -> bool {
582 let Some(opts) = args.get(2) else {
583 return false;
584 };
585 let Node::DictLiteral(entries) = &opts.node else {
586 return false;
587 };
588 entries.iter().any(|entry| {
589 let key = match &entry.key.node {
590 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
591 _ => return false,
592 };
593 key == "output" && output_schema_type_expr_from_node(&entry.value, scope).is_some()
594 })
595 }
596
597 pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
600 matches!(
601 ty,
602 TypeExpr::Shape(_)
603 | TypeExpr::Applied { .. }
604 | TypeExpr::FnType { .. }
605 | TypeExpr::List(_)
606 | TypeExpr::Iter(_)
607 | TypeExpr::Generator(_)
608 | TypeExpr::Stream(_)
609 | TypeExpr::DictType(_, _)
610 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
611 }
612
613 pub fn check_with_hints(
615 mut self,
616 program: &[SNode],
617 source: &str,
618 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
619 self.source = Some(source.to_string());
620 let facts = self.check_inner(program);
621 (facts.diagnostics, facts.inlay_hints)
622 }
623
624 pub fn check_with_facts(mut self, program: &[SNode], source: &str) -> TypeCheckFacts {
626 self.source = Some(source.to_string());
627 self.check_inner(program)
628 }
629
630 pub(in crate::typechecker) fn error_at(&mut self, code: Code, message: String, span: Span) {
631 self.diagnostics.push(TypeDiagnostic {
632 code,
633 message,
634 severity: DiagnosticSeverity::Error,
635 span: Some(span),
636 help: None,
637 related: Vec::new(),
638 fix: None,
639 details: None,
640 repair: default_repair(code),
641 });
642 }
643
644 #[allow(dead_code)]
645 pub(in crate::typechecker) fn error_at_with_help(
646 &mut self,
647 code: Code,
648 message: String,
649 span: Span,
650 help: String,
651 ) {
652 self.diagnostics.push(TypeDiagnostic {
653 code,
654 message,
655 severity: DiagnosticSeverity::Error,
656 span: Some(span),
657 help: Some(help),
658 related: Vec::new(),
659 fix: None,
660 details: None,
661 repair: default_repair(code),
662 });
663 }
664
665 pub(in crate::typechecker) fn unresolved_name_error_at(
666 &mut self,
667 name: &str,
668 message: String,
669 span: Span,
670 help: Option<String>,
671 ) {
672 self.diagnostics.push(TypeDiagnostic {
673 code: Code::UndefinedVariable,
674 message,
675 severity: DiagnosticSeverity::Error,
676 span: Some(span),
677 help,
678 related: Vec::new(),
679 fix: None,
680 details: Some(DiagnosticDetails::UnresolvedName {
681 name: name.to_string(),
682 }),
683 repair: default_repair(Code::UndefinedVariable),
684 });
685 }
686
687 pub(in crate::typechecker) fn flow_capability_boundary_error_at(
688 &mut self,
689 parameter: &str,
690 capabilities: Vec<String>,
691 span: Span,
692 ) {
693 let capabilities_display = capabilities.join(", ");
694 self.diagnostics.push(TypeDiagnostic {
695 code: Code::FlowInvariantAttributeInvalid,
696 message: format!(
697 "Flow `@invariant` parameter `{parameter}` requests unsupported capability authority: {capabilities_display}; Flow evaluation injects only a leading `HarnessAst`"
698 ),
699 severity: DiagnosticSeverity::Error,
700 span: Some(span),
701 help: Some(
702 "move the effect outside the predicate or accept the injected `HarnessAst` as its first parameter"
703 .to_string(),
704 ),
705 related: Vec::new(),
706 fix: None,
707 details: Some(DiagnosticDetails::FlowCapabilityBoundary {
708 parameter: parameter.to_string(),
709 capabilities,
710 allowed: vec!["HarnessAst".to_string()],
711 }),
712 repair: default_repair(Code::FlowInvariantAttributeInvalid),
713 });
714 }
715
716 pub(in crate::typechecker) fn type_mismatch_at(
717 &mut self,
718 code: Code,
719 context: impl Into<String>,
720 expected: &TypeExpr,
721 actual: &TypeExpr,
722 span: Span,
723 evidence: TypeMismatchEvidence,
724 scope: &TypeScope,
725 ) {
726 let (expected_origin, value_span) = evidence;
727 let nested_mismatch = first_nested_mismatch(expected, actual, scope);
728 let mut message = format!(
729 "{}: expected {}, found {}",
730 context.into(),
731 format_type(expected),
732 format_type(actual)
733 );
734 if let Some(detail) = shape_mismatch_detail(expected, actual)
735 .or_else(|| nested_mismatch.as_ref().map(|note| note.message.clone()))
736 {
737 message.push_str(&format!(" ({detail})"));
738 }
739
740 let mut related = Vec::new();
741 if let Some((span, message)) = expected_origin {
742 related.push(RelatedDiagnostic { span, message });
743 }
744 if let Some(note) = nested_mismatch {
745 related.push(RelatedDiagnostic {
746 span,
747 message: format!("nested mismatch: {}", note.message),
748 });
749 }
750
751 self.diagnostics.push(TypeDiagnostic {
752 code,
753 message,
754 severity: DiagnosticSeverity::Error,
755 span: Some(span),
756 help: coercion_suggestion(expected, actual, value_span, self.source.as_deref()),
757 related,
758 fix: None,
759 details: Some(DiagnosticDetails::TypeMismatch {
760 expected: expected.clone(),
761 actual: actual.clone(),
762 }),
763 repair: default_repair(code),
764 });
765 }
766
767 pub(in crate::typechecker) fn error_at_with_fix(
768 &mut self,
769 code: Code,
770 message: String,
771 span: Span,
772 fix: Vec<FixEdit>,
773 ) {
774 self.diagnostics.push(TypeDiagnostic {
775 code,
776 message,
777 severity: DiagnosticSeverity::Error,
778 span: Some(span),
779 help: None,
780 related: Vec::new(),
781 fix: Some(fix),
782 details: None,
783 repair: default_repair(code),
784 });
785 }
786
787 pub(in crate::typechecker) fn exhaustiveness_error_with_missing(
793 &mut self,
794 code: Code,
795 message: String,
796 span: Span,
797 missing: Vec<String>,
798 ) {
799 self.diagnostics.push(TypeDiagnostic {
800 code,
801 message,
802 severity: DiagnosticSeverity::Error,
803 span: Some(span),
804 help: None,
805 related: Vec::new(),
806 fix: None,
807 details: Some(DiagnosticDetails::NonExhaustiveMatch { missing }),
808 repair: default_repair(code),
809 });
810 }
811
812 pub(in crate::typechecker) fn warning_at(&mut self, code: Code, message: String, span: Span) {
813 self.diagnostics.push(TypeDiagnostic {
814 code,
815 message,
816 severity: DiagnosticSeverity::Warning,
817 span: Some(span),
818 help: None,
819 related: Vec::new(),
820 fix: None,
821 details: None,
822 repair: default_repair(code),
823 });
824 }
825
826 pub(in crate::typechecker) fn call_arity_warning_at(
827 &mut self,
828 code: Code,
829 message: String,
830 span: Span,
831 callee: &str,
832 parameter_types: Vec<Option<TypeExpr>>,
833 required: usize,
834 actual: usize,
835 ) {
836 self.diagnostics.push(TypeDiagnostic {
837 code,
838 message,
839 severity: DiagnosticSeverity::Warning,
840 span: Some(span),
841 help: None,
842 related: Vec::new(),
843 fix: None,
844 details: Some(DiagnosticDetails::CallArity {
845 callee: callee.to_string(),
846 parameter_types,
847 required,
848 actual,
849 }),
850 repair: default_repair(code),
851 });
852 }
853
854 #[allow(dead_code)]
855 pub(in crate::typechecker) fn warning_at_with_help(
856 &mut self,
857 code: Code,
858 message: String,
859 span: Span,
860 help: String,
861 ) {
862 self.diagnostics.push(TypeDiagnostic {
863 code,
864 message,
865 severity: DiagnosticSeverity::Warning,
866 span: Some(span),
867 help: Some(help),
868 related: Vec::new(),
869 fix: None,
870 details: None,
871 repair: default_repair(code),
872 });
873 }
874
875 pub(in crate::typechecker) fn lint_warning_at_with_fix(
876 &mut self,
877 code: Code,
878 rule: &'static str,
879 message: String,
880 span: Span,
881 help: String,
882 fix: Vec<FixEdit>,
883 ) {
884 self.diagnostics.push(TypeDiagnostic {
885 code,
886 message,
887 severity: DiagnosticSeverity::Warning,
888 span: Some(span),
889 help: Some(help),
890 related: Vec::new(),
891 fix: Some(fix),
892 details: Some(DiagnosticDetails::LintRule { rule }),
893 repair: default_repair(code),
894 });
895 }
896}
897
898pub(crate) fn default_repair(code: Code) -> Option<Repair> {
903 code.repair_template().map(Repair::from_template)
904}
905
906#[derive(Debug)]
907struct MismatchNote {
908 message: String,
909}
910
911fn first_nested_mismatch(
912 expected: &TypeExpr,
913 actual: &TypeExpr,
914 scope: &TypeScope,
915) -> Option<MismatchNote> {
916 let expected = resolve_type_for_diagnostic(expected, scope);
917 let actual = resolve_type_for_diagnostic(actual, scope);
918 match (&expected, &actual) {
919 (TypeExpr::Shape(expected_fields), TypeExpr::Shape(actual_fields)) => {
920 for expected_field in expected_fields {
921 if expected_field.optional {
922 continue;
923 }
924 let Some(actual_field) = actual_fields
925 .iter()
926 .find(|actual_field| actual_field.name == expected_field.name)
927 else {
928 return Some(MismatchNote {
929 message: format!(
930 "field `{}` is missing; expected {}",
931 expected_field.name,
932 format_type(&expected_field.type_expr)
933 ),
934 });
935 };
936 if !types_compatible_for_diagnostic(
937 &expected_field.type_expr,
938 &actual_field.type_expr,
939 scope,
940 ) {
941 return Some(MismatchNote {
942 message: format!(
943 "field `{}` expected {}, found {}",
944 expected_field.name,
945 format_type(&expected_field.type_expr),
946 format_type(&actual_field.type_expr)
947 ),
948 });
949 }
950 }
951 None
952 }
953 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
954 if !types_compatible_for_diagnostic(expected_inner, actual_inner, scope)
955 || !types_compatible_for_diagnostic(actual_inner, expected_inner, scope)
956 {
957 Some(MismatchNote {
958 message: format!(
959 "list element expected {}, found {}",
960 format_type(expected_inner),
961 format_type(actual_inner)
962 ),
963 })
964 } else {
965 None
966 }
967 }
968 (
969 TypeExpr::DictType(expected_key, expected_value),
970 TypeExpr::DictType(actual_key, actual_value),
971 ) => {
972 if !types_compatible_for_diagnostic(expected_key, actual_key, scope)
973 || !types_compatible_for_diagnostic(actual_key, expected_key, scope)
974 {
975 Some(MismatchNote {
976 message: format!(
977 "dict key expected {}, found {}",
978 format_type(expected_key),
979 format_type(actual_key)
980 ),
981 })
982 } else if !types_compatible_for_diagnostic(expected_value, actual_value, scope)
983 || !types_compatible_for_diagnostic(actual_value, expected_value, scope)
984 {
985 Some(MismatchNote {
986 message: format!(
987 "dict value expected {}, found {}",
988 format_type(expected_value),
989 format_type(actual_value)
990 ),
991 })
992 } else {
993 None
994 }
995 }
996 (
997 TypeExpr::Applied {
998 name: expected_name,
999 args: expected_args,
1000 },
1001 TypeExpr::Applied {
1002 name: actual_name,
1003 args: actual_args,
1004 },
1005 ) if expected_name == actual_name => expected_args
1006 .iter()
1007 .zip(actual_args.iter())
1008 .enumerate()
1009 .find_map(|(idx, (expected_arg, actual_arg))| {
1010 if types_compatible_for_diagnostic(expected_arg, actual_arg, scope)
1011 && types_compatible_for_diagnostic(actual_arg, expected_arg, scope)
1012 {
1013 None
1014 } else {
1015 Some(MismatchNote {
1016 message: format!(
1017 "{} type argument {} expected {}, found {}",
1018 expected_name,
1019 idx + 1,
1020 format_type(expected_arg),
1021 format_type(actual_arg)
1022 ),
1023 })
1024 }
1025 }),
1026 (
1027 TypeExpr::FnType {
1028 params: expected_params,
1029 return_type: expected_return,
1030 },
1031 TypeExpr::FnType {
1032 params: actual_params,
1033 return_type: actual_return,
1034 },
1035 ) => {
1036 for (idx, (expected_param, actual_param)) in
1037 expected_params.iter().zip(actual_params.iter()).enumerate()
1038 {
1039 if !types_compatible_for_diagnostic(actual_param, expected_param, scope) {
1040 return Some(MismatchNote {
1041 message: format!(
1042 "function parameter {} expected {}, found {}",
1043 idx + 1,
1044 format_type(expected_param),
1045 format_type(actual_param)
1046 ),
1047 });
1048 }
1049 }
1050 if !types_compatible_for_diagnostic(expected_return, actual_return, scope) {
1051 Some(MismatchNote {
1052 message: format!(
1053 "function return expected {}, found {}",
1054 format_type(expected_return),
1055 format_type(actual_return)
1056 ),
1057 })
1058 } else {
1059 None
1060 }
1061 }
1062 _ => None,
1063 }
1064}
1065
1066fn types_compatible_for_diagnostic(
1067 expected: &TypeExpr,
1068 actual: &TypeExpr,
1069 scope: &TypeScope,
1070) -> bool {
1071 TypeChecker::new().types_compatible(expected, actual, scope)
1072}
1073
1074fn resolve_type_for_diagnostic(ty: &TypeExpr, scope: &TypeScope) -> TypeExpr {
1075 TypeChecker::new().resolve_alias(ty, scope)
1076}
1077
1078fn coercion_suggestion(
1079 expected: &TypeExpr,
1080 actual: &TypeExpr,
1081 value_span: Option<Span>,
1082 source: Option<&str>,
1083) -> Option<String> {
1084 let expr = value_span
1085 .and_then(|span| source.and_then(|source| source.get(span.start..span.end)))
1086 .map(str::trim)
1087 .filter(|expr| !expr.is_empty());
1088 if is_nilable(actual) {
1089 return Some("handle `nil` first or provide a default with `??`".to_string());
1090 }
1091 let expected_ty = expected;
1092 let expected = simple_type_name(expected)?;
1093 let actual_name = simple_type_name(actual)?;
1094 let with_expr = |template: &str| {
1095 expr.map(|expr| template.replace("{}", expr))
1096 .unwrap_or_else(|| template.replace("{}", "value"))
1097 };
1098
1099 match (expected, actual_name) {
1100 ("string", "int" | "float" | "bool" | "nil" | "duration") => {
1101 Some(format!("did you mean `{}`?", with_expr("to_string({})")))
1102 }
1103 ("int", "string") => Some(format!("did you mean `{}`?", with_expr("to_int({})"))),
1104 ("float", "string" | "int") => {
1105 Some(format!("did you mean `{}`?", with_expr("to_float({})")))
1106 }
1107 (_, "nil") => Some("handle `nil` first or provide a default with `??`".to_string()),
1108 _ if actual_is_result_of(expected_ty, actual) => Some(format!(
1109 "did you mean `{}` or `{}`?",
1110 with_expr("{}?"),
1111 with_expr("unwrap_or({}, default)")
1112 )),
1113 _ => None,
1114 }
1115}
1116
1117fn simple_type_name(ty: &TypeExpr) -> Option<&str> {
1118 match ty {
1119 TypeExpr::Named(name) => Some(name.as_str()),
1120 TypeExpr::LitString(_) => Some("string"),
1121 TypeExpr::LitInt(_) => Some("int"),
1122 _ => None,
1123 }
1124}
1125
1126fn is_nilable(ty: &TypeExpr) -> bool {
1127 match ty {
1128 TypeExpr::Union(members) if members.len() == 2 => members
1129 .iter()
1130 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil")),
1131 _ => false,
1132 }
1133}
1134
1135fn actual_is_result_of(expected: &TypeExpr, actual: &TypeExpr) -> bool {
1136 matches!(
1137 actual,
1138 TypeExpr::Applied { name, args }
1139 if name == "Result" && args.first().is_some_and(|ok| ok == expected)
1140 )
1141}
1142
1143pub(in crate::typechecker) fn is_gradual_type_name(name: &str) -> bool {
1151 matches!(name, "any" | "unknown" | "_")
1152}
1153
1154impl Default for TypeChecker {
1155 fn default() -> Self {
1156 Self::new()
1157 }
1158}
1159
1160#[cfg(test)]
1161mod tests;