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