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