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