1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::rc::Rc;
3
4use crate::ast::*;
5use crate::builtin_signatures;
6use crate::diagnostic_codes::{Code, Repair};
7use harn_lexer::{FixEdit, Span};
8
9type TypeMismatchEvidence = (Option<(Span, String)>, Option<Span>);
10
11mod binary_ops;
12mod exits;
13mod format;
14mod inference;
15pub mod method_registry;
16mod schema_inference;
17mod scope;
18mod union;
19
20pub use exits::{block_definitely_exits, stmt_definitely_exits};
21pub use format::{format_type, shape_mismatch_detail};
22
23pub fn substitute_type_expr(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
27 TypeChecker::apply_type_bindings(ty, bindings)
28}
29
30use schema_inference::output_schema_type_expr_from_node;
31use scope::TypeScope;
32
33#[derive(Debug, Clone)]
35pub struct InlayHintInfo {
36 pub line: usize,
38 pub column: usize,
39 pub label: String,
41}
42
43#[derive(Debug, Clone)]
45pub struct NamespaceImportBinding {
46 pub module_path: String,
48 pub members: BTreeSet<String>,
50}
51
52#[derive(Debug, Clone)]
54pub struct TypeDiagnostic {
55 pub code: Code,
56 pub message: String,
57 pub severity: DiagnosticSeverity,
58 pub span: Option<Span>,
59 pub help: Option<String>,
60 pub related: Vec<RelatedDiagnostic>,
61 pub fix: Option<Vec<FixEdit>>,
63 pub details: Option<DiagnosticDetails>,
68 pub repair: Option<Repair>,
74}
75
76#[derive(Debug, Clone)]
77pub struct RelatedDiagnostic {
78 pub span: Span,
79 pub message: String,
80}
81
82#[derive(Debug, Clone)]
89pub enum DiagnosticDetails {
90 TypeMismatch,
93 NonExhaustiveMatch { missing: Vec<String> },
100 LintRule { rule: &'static str },
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum DiagnosticSeverity {
109 Error,
110 Warning,
111}
112
113pub struct TypeChecker {
115 diagnostics: Vec<TypeDiagnostic>,
116 scope: Rc<TypeScope>,
122 source: Option<String>,
123 hints: Vec<InlayHintInfo>,
124 strict_types: bool,
126 legacy_ambient_capabilities: bool,
130 fn_depth: usize,
133 stream_fn_depth: usize,
135 stream_emit_types: Vec<Option<TypeExpr>>,
137 expected_return_types: Vec<Option<TypeExpr>>,
141 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
146 imported_names: Option<HashSet<String>>,
153 imported_type_decls: Vec<SNode>,
157 imported_callable_decls: Vec<SNode>,
160 namespace_imports: std::collections::HashMap<String, NamespaceImportBinding>,
163 const_env: crate::const_eval::ConstEnv,
167 subtype_cycle_guard: std::cell::RefCell<Vec<(TypeExpr, TypeExpr)>>,
175}
176
177impl TypeChecker {
178 pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
179 TypeExpr::Named("_".into())
180 }
181
182 pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
183 matches!(ty, TypeExpr::Named(name) if name == "_")
184 }
185
186 pub(in crate::typechecker) fn contains_wildcard_type(ty: &TypeExpr) -> bool {
187 match ty {
188 TypeExpr::Named(name) => name == "_",
189 TypeExpr::Union(members) | TypeExpr::Intersection(members) => {
190 members.iter().any(Self::contains_wildcard_type)
191 }
192 TypeExpr::Tuple(items) => items.iter().any(Self::contains_wildcard_type),
193 TypeExpr::Shape(fields) => fields
194 .iter()
195 .any(|field| Self::contains_wildcard_type(&field.type_expr)),
196 TypeExpr::OpenShape { fields, rests } => {
197 fields
198 .iter()
199 .any(|field| Self::contains_wildcard_type(&field.type_expr))
200 || rests.iter().any(Self::contains_wildcard_type)
201 }
202 TypeExpr::List(inner)
203 | TypeExpr::Iter(inner)
204 | TypeExpr::Generator(inner)
205 | TypeExpr::Stream(inner)
206 | TypeExpr::Owned(inner) => Self::contains_wildcard_type(inner),
207 TypeExpr::DictType(key, value) => {
208 Self::contains_wildcard_type(key) || Self::contains_wildcard_type(value)
209 }
210 TypeExpr::Applied { args, .. } => args.iter().any(Self::contains_wildcard_type),
211 TypeExpr::FnType {
212 params,
213 return_type,
214 } => {
215 params.iter().any(Self::contains_wildcard_type)
216 || Self::contains_wildcard_type(return_type)
217 }
218 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
219 }
220 }
221
222 pub(in crate::typechecker) fn contains_type_param(
223 ty: &TypeExpr,
224 type_params: &BTreeSet<String>,
225 ) -> bool {
226 match ty {
227 TypeExpr::Named(name) => type_params.contains(name),
228 TypeExpr::Union(members) | TypeExpr::Intersection(members) => members
229 .iter()
230 .any(|member| Self::contains_type_param(member, type_params)),
231 TypeExpr::Tuple(items) => items
232 .iter()
233 .any(|item| Self::contains_type_param(item, type_params)),
234 TypeExpr::Shape(fields) => fields
235 .iter()
236 .any(|field| Self::contains_type_param(&field.type_expr, type_params)),
237 TypeExpr::OpenShape { fields, rests } => {
238 fields
239 .iter()
240 .any(|field| Self::contains_type_param(&field.type_expr, type_params))
241 || rests
242 .iter()
243 .any(|rest| Self::contains_type_param(rest, type_params))
244 }
245 TypeExpr::List(inner)
246 | TypeExpr::Iter(inner)
247 | TypeExpr::Generator(inner)
248 | TypeExpr::Stream(inner)
249 | TypeExpr::Owned(inner) => Self::contains_type_param(inner, type_params),
250 TypeExpr::DictType(key, value) => {
251 Self::contains_type_param(key, type_params)
252 || Self::contains_type_param(value, type_params)
253 }
254 TypeExpr::Applied { args, .. } => args
255 .iter()
256 .any(|arg| Self::contains_type_param(arg, type_params)),
257 TypeExpr::FnType {
258 params,
259 return_type,
260 } => {
261 params
262 .iter()
263 .any(|param| Self::contains_type_param(param, type_params))
264 || Self::contains_type_param(return_type, type_params)
265 }
266 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
267 }
268 }
269
270 pub(in crate::typechecker) fn contains_abstract_type(
271 &self,
272 ty: &TypeExpr,
273 scope: &TypeScope,
274 ) -> bool {
275 match ty {
276 TypeExpr::Named(name) => {
277 matches!(name.as_str(), "_" | "any" | "unknown")
278 || scope.is_generic_type_param(name)
279 }
280 TypeExpr::Union(members) | TypeExpr::Intersection(members) => members
281 .iter()
282 .any(|member| self.contains_abstract_type(member, scope)),
283 TypeExpr::Tuple(items) => items
284 .iter()
285 .any(|item| self.contains_abstract_type(item, scope)),
286 TypeExpr::Shape(fields) => fields
287 .iter()
288 .any(|field| self.contains_abstract_type(&field.type_expr, scope)),
289 TypeExpr::OpenShape { fields, rests } => {
290 fields
291 .iter()
292 .any(|field| self.contains_abstract_type(&field.type_expr, scope))
293 || rests
294 .iter()
295 .any(|rest| self.contains_abstract_type(rest, scope))
296 }
297 TypeExpr::List(inner)
298 | TypeExpr::Iter(inner)
299 | TypeExpr::Generator(inner)
300 | TypeExpr::Stream(inner)
301 | TypeExpr::Owned(inner) => self.contains_abstract_type(inner, scope),
302 TypeExpr::DictType(key, value) => {
303 self.contains_abstract_type(key, scope) || self.contains_abstract_type(value, scope)
304 }
305 TypeExpr::Applied { args, .. } => args
306 .iter()
307 .any(|arg| self.contains_abstract_type(arg, scope)),
308 TypeExpr::FnType {
309 params,
310 return_type,
311 } => {
312 params
313 .iter()
314 .any(|param| self.contains_abstract_type(param, scope))
315 || self.contains_abstract_type(return_type, scope)
316 }
317 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
318 }
319 }
320
321 pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
322 match ty {
323 TypeExpr::Named(name) => Some(name.as_str()),
324 TypeExpr::Applied { name, .. } => Some(name.as_str()),
325 _ => None,
326 }
327 }
328
329 pub fn new() -> Self {
330 Self {
331 diagnostics: Vec::new(),
332 scope: Rc::new(TypeScope::new()),
333 source: None,
334 hints: Vec::new(),
335 strict_types: false,
336 legacy_ambient_capabilities: crate::legacy_ambient_capabilities_enabled(),
337 fn_depth: 0,
338 stream_fn_depth: 0,
339 stream_emit_types: Vec::new(),
340 expected_return_types: Vec::new(),
341 deprecated_fns: std::collections::HashMap::new(),
342 imported_names: None,
343 imported_type_decls: Vec::new(),
344 imported_callable_decls: Vec::new(),
345 namespace_imports: std::collections::HashMap::new(),
346 const_env: crate::const_eval::ConstEnv::new(),
347 subtype_cycle_guard: std::cell::RefCell::new(Vec::new()),
348 }
349 }
350
351 pub fn with_strict_types(strict: bool) -> Self {
354 Self {
355 diagnostics: Vec::new(),
356 scope: Rc::new(TypeScope::new()),
357 source: None,
358 hints: Vec::new(),
359 strict_types: strict,
360 legacy_ambient_capabilities: crate::legacy_ambient_capabilities_enabled(),
361 fn_depth: 0,
362 stream_fn_depth: 0,
363 stream_emit_types: Vec::new(),
364 expected_return_types: Vec::new(),
365 deprecated_fns: std::collections::HashMap::new(),
366 imported_names: None,
367 imported_type_decls: Vec::new(),
368 imported_callable_decls: Vec::new(),
369 namespace_imports: std::collections::HashMap::new(),
370 const_env: crate::const_eval::ConstEnv::new(),
371 subtype_cycle_guard: std::cell::RefCell::new(Vec::new()),
372 }
373 }
374
375 pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
386 self.imported_names = Some(imported);
387 self
388 }
389
390 #[cfg(test)]
391 pub(crate) fn with_legacy_ambient_capabilities(mut self) -> Self {
392 self.legacy_ambient_capabilities = true;
393 self
394 }
395
396 pub fn with_imported_type_decls(mut self, imported: Vec<SNode>) -> Self {
400 self.imported_type_decls = imported;
401 self
402 }
403
404 pub fn with_imported_callable_decls(mut self, imported: Vec<SNode>) -> Self {
409 self.imported_callable_decls = imported;
410 self
411 }
412
413 pub fn with_namespace_imports(
419 mut self,
420 imports: impl IntoIterator<Item = (String, NamespaceImportBinding)>,
421 ) -> Self {
422 let imports: std::collections::HashMap<String, NamespaceImportBinding> =
423 imports.into_iter().collect();
424 if let Some(names) = self.imported_names.as_mut() {
425 for alias in imports.keys() {
426 names.insert(alias.clone());
427 }
428 }
429 self.namespace_imports = imports;
430 self
431 }
432
433 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
435 self.source = Some(source.to_string());
436 self.check_inner(program).0
437 }
438
439 pub fn check_strict_with_source(
441 mut self,
442 program: &[SNode],
443 source: &str,
444 ) -> Vec<TypeDiagnostic> {
445 self.source = Some(source.to_string());
446 self.strict_types = true;
447 self.check_inner(program).0
448 }
449
450 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
452 self.check_inner(program).0
453 }
454
455 pub(in crate::typechecker) fn detect_boundary_source(
459 value: &SNode,
460 scope: &TypeScope,
461 ) -> Option<String> {
462 match &value.node {
463 Node::FunctionCall { name, args, .. } => {
464 if !builtin_signatures::is_untyped_boundary_source(name) {
465 return None;
466 }
467 if (name == "llm_call" || name == "llm_completion")
469 && Self::llm_call_has_typed_schema_option(args, scope)
470 {
471 return None;
472 }
473 Some(name.clone())
474 }
475 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
476 _ => None,
477 }
478 }
479
480 pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
486 args: &[SNode],
487 scope: &TypeScope,
488 ) -> bool {
489 let Some(opts) = args.get(2) else {
490 return false;
491 };
492 let Node::DictLiteral(entries) = &opts.node else {
493 return false;
494 };
495 entries.iter().any(|entry| {
496 let key = match &entry.key.node {
497 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
498 _ => return false,
499 };
500 key == "output" && output_schema_type_expr_from_node(&entry.value, scope).is_some()
501 })
502 }
503
504 pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
507 matches!(
508 ty,
509 TypeExpr::Shape(_)
510 | TypeExpr::Applied { .. }
511 | TypeExpr::FnType { .. }
512 | TypeExpr::List(_)
513 | TypeExpr::Iter(_)
514 | TypeExpr::Generator(_)
515 | TypeExpr::Stream(_)
516 | TypeExpr::DictType(_, _)
517 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
518 }
519
520 pub fn check_with_hints(
522 mut self,
523 program: &[SNode],
524 source: &str,
525 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
526 self.source = Some(source.to_string());
527 self.check_inner(program)
528 }
529
530 pub(in crate::typechecker) fn error_at(&mut self, code: Code, message: String, span: Span) {
531 self.diagnostics.push(TypeDiagnostic {
532 code,
533 message,
534 severity: DiagnosticSeverity::Error,
535 span: Some(span),
536 help: None,
537 related: Vec::new(),
538 fix: None,
539 details: None,
540 repair: default_repair(code),
541 });
542 }
543
544 #[allow(dead_code)]
545 pub(in crate::typechecker) fn error_at_with_help(
546 &mut self,
547 code: Code,
548 message: String,
549 span: Span,
550 help: String,
551 ) {
552 self.diagnostics.push(TypeDiagnostic {
553 code,
554 message,
555 severity: DiagnosticSeverity::Error,
556 span: Some(span),
557 help: Some(help),
558 related: Vec::new(),
559 fix: None,
560 details: None,
561 repair: default_repair(code),
562 });
563 }
564
565 pub(in crate::typechecker) fn type_mismatch_at(
566 &mut self,
567 code: Code,
568 context: impl Into<String>,
569 expected: &TypeExpr,
570 actual: &TypeExpr,
571 span: Span,
572 evidence: TypeMismatchEvidence,
573 scope: &TypeScope,
574 ) {
575 let (expected_origin, value_span) = evidence;
576 let nested_mismatch = first_nested_mismatch(expected, actual, scope);
577 let mut message = format!(
578 "{}: expected {}, found {}",
579 context.into(),
580 format_type(expected),
581 format_type(actual)
582 );
583 if let Some(detail) = shape_mismatch_detail(expected, actual)
584 .or_else(|| nested_mismatch.as_ref().map(|note| note.message.clone()))
585 {
586 message.push_str(&format!(" ({detail})"));
587 }
588
589 let mut related = Vec::new();
590 if let Some((span, message)) = expected_origin {
591 related.push(RelatedDiagnostic { span, message });
592 }
593 if let Some(note) = nested_mismatch {
594 related.push(RelatedDiagnostic {
595 span,
596 message: format!("nested mismatch: {}", note.message),
597 });
598 }
599
600 self.diagnostics.push(TypeDiagnostic {
601 code,
602 message,
603 severity: DiagnosticSeverity::Error,
604 span: Some(span),
605 help: coercion_suggestion(expected, actual, value_span, self.source.as_deref()),
606 related,
607 fix: None,
608 details: Some(DiagnosticDetails::TypeMismatch),
609 repair: default_repair(code),
610 });
611 }
612
613 pub(in crate::typechecker) fn error_at_with_fix(
614 &mut self,
615 code: Code,
616 message: String,
617 span: Span,
618 fix: Vec<FixEdit>,
619 ) {
620 self.diagnostics.push(TypeDiagnostic {
621 code,
622 message,
623 severity: DiagnosticSeverity::Error,
624 span: Some(span),
625 help: None,
626 related: Vec::new(),
627 fix: Some(fix),
628 details: None,
629 repair: default_repair(code),
630 });
631 }
632
633 pub(in crate::typechecker) fn exhaustiveness_error_with_missing(
639 &mut self,
640 code: Code,
641 message: String,
642 span: Span,
643 missing: Vec<String>,
644 ) {
645 self.diagnostics.push(TypeDiagnostic {
646 code,
647 message,
648 severity: DiagnosticSeverity::Error,
649 span: Some(span),
650 help: None,
651 related: Vec::new(),
652 fix: None,
653 details: Some(DiagnosticDetails::NonExhaustiveMatch { missing }),
654 repair: default_repair(code),
655 });
656 }
657
658 pub(in crate::typechecker) fn warning_at(&mut self, code: Code, message: String, span: Span) {
659 self.diagnostics.push(TypeDiagnostic {
660 code,
661 message,
662 severity: DiagnosticSeverity::Warning,
663 span: Some(span),
664 help: None,
665 related: Vec::new(),
666 fix: None,
667 details: None,
668 repair: default_repair(code),
669 });
670 }
671
672 #[allow(dead_code)]
673 pub(in crate::typechecker) fn warning_at_with_help(
674 &mut self,
675 code: Code,
676 message: String,
677 span: Span,
678 help: String,
679 ) {
680 self.diagnostics.push(TypeDiagnostic {
681 code,
682 message,
683 severity: DiagnosticSeverity::Warning,
684 span: Some(span),
685 help: Some(help),
686 related: Vec::new(),
687 fix: None,
688 details: None,
689 repair: default_repair(code),
690 });
691 }
692
693 pub(in crate::typechecker) fn lint_warning_at_with_fix(
694 &mut self,
695 code: Code,
696 rule: &'static str,
697 message: String,
698 span: Span,
699 help: String,
700 fix: Vec<FixEdit>,
701 ) {
702 self.diagnostics.push(TypeDiagnostic {
703 code,
704 message,
705 severity: DiagnosticSeverity::Warning,
706 span: Some(span),
707 help: Some(help),
708 related: Vec::new(),
709 fix: Some(fix),
710 details: Some(DiagnosticDetails::LintRule { rule }),
711 repair: default_repair(code),
712 });
713 }
714}
715
716pub(crate) fn default_repair(code: Code) -> Option<Repair> {
721 code.repair_template().map(Repair::from_template)
722}
723
724#[derive(Debug)]
725struct MismatchNote {
726 message: String,
727}
728
729fn first_nested_mismatch(
730 expected: &TypeExpr,
731 actual: &TypeExpr,
732 scope: &TypeScope,
733) -> Option<MismatchNote> {
734 let expected = resolve_type_for_diagnostic(expected, scope);
735 let actual = resolve_type_for_diagnostic(actual, scope);
736 match (&expected, &actual) {
737 (TypeExpr::Shape(expected_fields), TypeExpr::Shape(actual_fields)) => {
738 for expected_field in expected_fields {
739 if expected_field.optional {
740 continue;
741 }
742 let Some(actual_field) = actual_fields
743 .iter()
744 .find(|actual_field| actual_field.name == expected_field.name)
745 else {
746 return Some(MismatchNote {
747 message: format!(
748 "field `{}` is missing; expected {}",
749 expected_field.name,
750 format_type(&expected_field.type_expr)
751 ),
752 });
753 };
754 if !types_compatible_for_diagnostic(
755 &expected_field.type_expr,
756 &actual_field.type_expr,
757 scope,
758 ) {
759 return Some(MismatchNote {
760 message: format!(
761 "field `{}` expected {}, found {}",
762 expected_field.name,
763 format_type(&expected_field.type_expr),
764 format_type(&actual_field.type_expr)
765 ),
766 });
767 }
768 }
769 None
770 }
771 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
772 if !types_compatible_for_diagnostic(expected_inner, actual_inner, scope)
773 || !types_compatible_for_diagnostic(actual_inner, expected_inner, scope)
774 {
775 Some(MismatchNote {
776 message: format!(
777 "list element expected {}, found {}",
778 format_type(expected_inner),
779 format_type(actual_inner)
780 ),
781 })
782 } else {
783 None
784 }
785 }
786 (
787 TypeExpr::DictType(expected_key, expected_value),
788 TypeExpr::DictType(actual_key, actual_value),
789 ) => {
790 if !types_compatible_for_diagnostic(expected_key, actual_key, scope)
791 || !types_compatible_for_diagnostic(actual_key, expected_key, scope)
792 {
793 Some(MismatchNote {
794 message: format!(
795 "dict key expected {}, found {}",
796 format_type(expected_key),
797 format_type(actual_key)
798 ),
799 })
800 } else if !types_compatible_for_diagnostic(expected_value, actual_value, scope)
801 || !types_compatible_for_diagnostic(actual_value, expected_value, scope)
802 {
803 Some(MismatchNote {
804 message: format!(
805 "dict value expected {}, found {}",
806 format_type(expected_value),
807 format_type(actual_value)
808 ),
809 })
810 } else {
811 None
812 }
813 }
814 (
815 TypeExpr::Applied {
816 name: expected_name,
817 args: expected_args,
818 },
819 TypeExpr::Applied {
820 name: actual_name,
821 args: actual_args,
822 },
823 ) if expected_name == actual_name => expected_args
824 .iter()
825 .zip(actual_args.iter())
826 .enumerate()
827 .find_map(|(idx, (expected_arg, actual_arg))| {
828 if types_compatible_for_diagnostic(expected_arg, actual_arg, scope)
829 && types_compatible_for_diagnostic(actual_arg, expected_arg, scope)
830 {
831 None
832 } else {
833 Some(MismatchNote {
834 message: format!(
835 "{} type argument {} expected {}, found {}",
836 expected_name,
837 idx + 1,
838 format_type(expected_arg),
839 format_type(actual_arg)
840 ),
841 })
842 }
843 }),
844 (
845 TypeExpr::FnType {
846 params: expected_params,
847 return_type: expected_return,
848 },
849 TypeExpr::FnType {
850 params: actual_params,
851 return_type: actual_return,
852 },
853 ) => {
854 for (idx, (expected_param, actual_param)) in
855 expected_params.iter().zip(actual_params.iter()).enumerate()
856 {
857 if !types_compatible_for_diagnostic(actual_param, expected_param, scope) {
858 return Some(MismatchNote {
859 message: format!(
860 "function parameter {} expected {}, found {}",
861 idx + 1,
862 format_type(expected_param),
863 format_type(actual_param)
864 ),
865 });
866 }
867 }
868 if !types_compatible_for_diagnostic(expected_return, actual_return, scope) {
869 Some(MismatchNote {
870 message: format!(
871 "function return expected {}, found {}",
872 format_type(expected_return),
873 format_type(actual_return)
874 ),
875 })
876 } else {
877 None
878 }
879 }
880 _ => None,
881 }
882}
883
884fn types_compatible_for_diagnostic(
885 expected: &TypeExpr,
886 actual: &TypeExpr,
887 scope: &TypeScope,
888) -> bool {
889 TypeChecker::new().types_compatible(expected, actual, scope)
890}
891
892fn resolve_type_for_diagnostic(ty: &TypeExpr, scope: &TypeScope) -> TypeExpr {
893 TypeChecker::new().resolve_alias(ty, scope)
894}
895
896fn coercion_suggestion(
897 expected: &TypeExpr,
898 actual: &TypeExpr,
899 value_span: Option<Span>,
900 source: Option<&str>,
901) -> Option<String> {
902 let expr = value_span
903 .and_then(|span| source.and_then(|source| source.get(span.start..span.end)))
904 .map(str::trim)
905 .filter(|expr| !expr.is_empty());
906 if is_nilable(actual) {
907 return Some("handle `nil` first or provide a default with `??`".to_string());
908 }
909 let expected_ty = expected;
910 let expected = simple_type_name(expected)?;
911 let actual_name = simple_type_name(actual)?;
912 let with_expr = |template: &str| {
913 expr.map(|expr| template.replace("{}", expr))
914 .unwrap_or_else(|| template.replace("{}", "value"))
915 };
916
917 match (expected, actual_name) {
918 ("string", "int" | "float" | "bool" | "nil" | "duration") => {
919 Some(format!("did you mean `{}`?", with_expr("to_string({})")))
920 }
921 ("int", "string") => Some(format!("did you mean `{}`?", with_expr("to_int({})"))),
922 ("float", "string" | "int") => {
923 Some(format!("did you mean `{}`?", with_expr("to_float({})")))
924 }
925 (_, "nil") => Some("handle `nil` first or provide a default with `??`".to_string()),
926 _ if actual_is_result_of(expected_ty, actual) => Some(format!(
927 "did you mean `{}` or `{}`?",
928 with_expr("{}?"),
929 with_expr("unwrap_or({}, default)")
930 )),
931 _ => None,
932 }
933}
934
935fn simple_type_name(ty: &TypeExpr) -> Option<&str> {
936 match ty {
937 TypeExpr::Named(name) => Some(name.as_str()),
938 TypeExpr::LitString(_) => Some("string"),
939 TypeExpr::LitInt(_) => Some("int"),
940 _ => None,
941 }
942}
943
944fn is_nilable(ty: &TypeExpr) -> bool {
945 match ty {
946 TypeExpr::Union(members) if members.len() == 2 => members
947 .iter()
948 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil")),
949 _ => false,
950 }
951}
952
953fn actual_is_result_of(expected: &TypeExpr, actual: &TypeExpr) -> bool {
954 matches!(
955 actual,
956 TypeExpr::Applied { name, args }
957 if name == "Result" && args.first().is_some_and(|ok| ok == expected)
958 )
959}
960
961pub(in crate::typechecker) fn is_gradual_type_name(name: &str) -> bool {
969 matches!(name, "any" | "unknown" | "_")
970}
971
972impl Default for TypeChecker {
973 fn default() -> Self {
974 Self::new()
975 }
976}
977
978#[cfg(test)]
979mod tests;