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