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