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