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