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