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