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