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