1use std::collections::{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;
15mod schema_inference;
16mod scope;
17mod union;
18
19pub use exits::{block_definitely_exits, stmt_definitely_exits};
20pub use format::{format_type, shape_mismatch_detail};
21
22use schema_inference::schema_type_expr_from_node;
23use scope::TypeScope;
24
25#[derive(Debug, Clone)]
27pub struct InlayHintInfo {
28 pub line: usize,
30 pub column: usize,
31 pub label: String,
33}
34
35#[derive(Debug, Clone)]
37pub struct TypeDiagnostic {
38 pub code: Code,
39 pub message: String,
40 pub severity: DiagnosticSeverity,
41 pub span: Option<Span>,
42 pub help: Option<String>,
43 pub related: Vec<RelatedDiagnostic>,
44 pub fix: Option<Vec<FixEdit>>,
46 pub details: Option<DiagnosticDetails>,
51 pub repair: Option<Repair>,
57}
58
59#[derive(Debug, Clone)]
60pub struct RelatedDiagnostic {
61 pub span: Span,
62 pub message: String,
63}
64
65#[derive(Debug, Clone)]
72pub enum DiagnosticDetails {
73 TypeMismatch,
76 NonExhaustiveMatch { missing: Vec<String> },
83 LintRule { rule: &'static str },
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum DiagnosticSeverity {
92 Error,
93 Warning,
94}
95
96pub struct TypeChecker {
98 diagnostics: Vec<TypeDiagnostic>,
99 scope: Rc<TypeScope>,
105 source: Option<String>,
106 hints: Vec<InlayHintInfo>,
107 strict_types: bool,
109 fn_depth: usize,
112 stream_fn_depth: usize,
114 stream_emit_types: Vec<Option<TypeExpr>>,
116 expected_return_types: Vec<Option<TypeExpr>>,
120 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
125 imported_names: Option<HashSet<String>>,
132 imported_type_decls: Vec<SNode>,
136 imported_callable_decls: Vec<SNode>,
139 const_env: crate::const_eval::ConstEnv,
143}
144
145impl TypeChecker {
146 pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
147 TypeExpr::Named("_".into())
148 }
149
150 pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
151 matches!(ty, TypeExpr::Named(name) if name == "_")
152 }
153
154 pub(in crate::typechecker) fn contains_wildcard_type(ty: &TypeExpr) -> bool {
155 match ty {
156 TypeExpr::Named(name) => name == "_",
157 TypeExpr::Union(members) | TypeExpr::Intersection(members) => {
158 members.iter().any(Self::contains_wildcard_type)
159 }
160 TypeExpr::Shape(fields) => fields
161 .iter()
162 .any(|field| Self::contains_wildcard_type(&field.type_expr)),
163 TypeExpr::OpenShape { fields, rests } => {
164 fields
165 .iter()
166 .any(|field| Self::contains_wildcard_type(&field.type_expr))
167 || rests.iter().any(Self::contains_wildcard_type)
168 }
169 TypeExpr::List(inner)
170 | TypeExpr::Iter(inner)
171 | TypeExpr::Generator(inner)
172 | TypeExpr::Stream(inner)
173 | TypeExpr::Owned(inner) => Self::contains_wildcard_type(inner),
174 TypeExpr::DictType(key, value) => {
175 Self::contains_wildcard_type(key) || Self::contains_wildcard_type(value)
176 }
177 TypeExpr::Applied { args, .. } => args.iter().any(Self::contains_wildcard_type),
178 TypeExpr::FnType {
179 params,
180 return_type,
181 } => {
182 params.iter().any(Self::contains_wildcard_type)
183 || Self::contains_wildcard_type(return_type)
184 }
185 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
186 }
187 }
188
189 pub(in crate::typechecker) fn contains_type_param(
190 ty: &TypeExpr,
191 type_params: &BTreeSet<String>,
192 ) -> bool {
193 match ty {
194 TypeExpr::Named(name) => type_params.contains(name),
195 TypeExpr::Union(members) | TypeExpr::Intersection(members) => members
196 .iter()
197 .any(|member| Self::contains_type_param(member, type_params)),
198 TypeExpr::Shape(fields) => fields
199 .iter()
200 .any(|field| Self::contains_type_param(&field.type_expr, type_params)),
201 TypeExpr::OpenShape { fields, rests } => {
202 fields
203 .iter()
204 .any(|field| Self::contains_type_param(&field.type_expr, type_params))
205 || rests
206 .iter()
207 .any(|rest| Self::contains_type_param(rest, type_params))
208 }
209 TypeExpr::List(inner)
210 | TypeExpr::Iter(inner)
211 | TypeExpr::Generator(inner)
212 | TypeExpr::Stream(inner)
213 | TypeExpr::Owned(inner) => Self::contains_type_param(inner, type_params),
214 TypeExpr::DictType(key, value) => {
215 Self::contains_type_param(key, type_params)
216 || Self::contains_type_param(value, type_params)
217 }
218 TypeExpr::Applied { args, .. } => args
219 .iter()
220 .any(|arg| Self::contains_type_param(arg, type_params)),
221 TypeExpr::FnType {
222 params,
223 return_type,
224 } => {
225 params
226 .iter()
227 .any(|param| Self::contains_type_param(param, type_params))
228 || Self::contains_type_param(return_type, type_params)
229 }
230 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
231 }
232 }
233
234 pub(in crate::typechecker) fn contains_abstract_type(
235 &self,
236 ty: &TypeExpr,
237 scope: &TypeScope,
238 ) -> bool {
239 match ty {
240 TypeExpr::Named(name) => {
241 matches!(name.as_str(), "_" | "any" | "unknown")
242 || scope.is_generic_type_param(name)
243 }
244 TypeExpr::Union(members) | TypeExpr::Intersection(members) => members
245 .iter()
246 .any(|member| self.contains_abstract_type(member, scope)),
247 TypeExpr::Shape(fields) => fields
248 .iter()
249 .any(|field| self.contains_abstract_type(&field.type_expr, scope)),
250 TypeExpr::OpenShape { fields, rests } => {
251 fields
252 .iter()
253 .any(|field| self.contains_abstract_type(&field.type_expr, scope))
254 || rests
255 .iter()
256 .any(|rest| self.contains_abstract_type(rest, scope))
257 }
258 TypeExpr::List(inner)
259 | TypeExpr::Iter(inner)
260 | TypeExpr::Generator(inner)
261 | TypeExpr::Stream(inner)
262 | TypeExpr::Owned(inner) => self.contains_abstract_type(inner, scope),
263 TypeExpr::DictType(key, value) => {
264 self.contains_abstract_type(key, scope) || self.contains_abstract_type(value, scope)
265 }
266 TypeExpr::Applied { args, .. } => args
267 .iter()
268 .any(|arg| self.contains_abstract_type(arg, scope)),
269 TypeExpr::FnType {
270 params,
271 return_type,
272 } => {
273 params
274 .iter()
275 .any(|param| self.contains_abstract_type(param, scope))
276 || self.contains_abstract_type(return_type, scope)
277 }
278 TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
279 }
280 }
281
282 pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
283 match ty {
284 TypeExpr::Named(name) => Some(name.as_str()),
285 TypeExpr::Applied { name, .. } => Some(name.as_str()),
286 _ => None,
287 }
288 }
289
290 pub fn new() -> Self {
291 Self {
292 diagnostics: Vec::new(),
293 scope: Rc::new(TypeScope::new()),
294 source: None,
295 hints: Vec::new(),
296 strict_types: false,
297 fn_depth: 0,
298 stream_fn_depth: 0,
299 stream_emit_types: Vec::new(),
300 expected_return_types: Vec::new(),
301 deprecated_fns: std::collections::HashMap::new(),
302 imported_names: None,
303 imported_type_decls: Vec::new(),
304 imported_callable_decls: Vec::new(),
305 const_env: crate::const_eval::ConstEnv::new(),
306 }
307 }
308
309 pub fn with_strict_types(strict: bool) -> Self {
312 Self {
313 diagnostics: Vec::new(),
314 scope: Rc::new(TypeScope::new()),
315 source: None,
316 hints: Vec::new(),
317 strict_types: strict,
318 fn_depth: 0,
319 stream_fn_depth: 0,
320 stream_emit_types: Vec::new(),
321 expected_return_types: Vec::new(),
322 deprecated_fns: std::collections::HashMap::new(),
323 imported_names: None,
324 imported_type_decls: Vec::new(),
325 imported_callable_decls: Vec::new(),
326 const_env: crate::const_eval::ConstEnv::new(),
327 }
328 }
329
330 pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
341 self.imported_names = Some(imported);
342 self
343 }
344
345 pub fn with_imported_type_decls(mut self, imported: Vec<SNode>) -> Self {
349 self.imported_type_decls = imported;
350 self
351 }
352
353 pub fn with_imported_callable_decls(mut self, imported: Vec<SNode>) -> Self {
358 self.imported_callable_decls = imported;
359 self
360 }
361
362 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
364 self.source = Some(source.to_string());
365 self.check_inner(program).0
366 }
367
368 pub fn check_strict_with_source(
370 mut self,
371 program: &[SNode],
372 source: &str,
373 ) -> Vec<TypeDiagnostic> {
374 self.source = Some(source.to_string());
375 self.strict_types = true;
376 self.check_inner(program).0
377 }
378
379 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
381 self.check_inner(program).0
382 }
383
384 pub(in crate::typechecker) fn detect_boundary_source(
388 value: &SNode,
389 scope: &TypeScope,
390 ) -> Option<String> {
391 match &value.node {
392 Node::FunctionCall { name, args, .. } => {
393 if !builtin_signatures::is_untyped_boundary_source(name) {
394 return None;
395 }
396 if (name == "llm_call" || name == "llm_completion")
398 && Self::llm_call_has_typed_schema_option(args, scope)
399 {
400 return None;
401 }
402 Some(name.clone())
403 }
404 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
405 _ => None,
406 }
407 }
408
409 pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
415 args: &[SNode],
416 scope: &TypeScope,
417 ) -> bool {
418 let Some(opts) = args.get(2) else {
419 return false;
420 };
421 let Node::DictLiteral(entries) = &opts.node else {
422 return false;
423 };
424 entries.iter().any(|entry| {
425 let key = match &entry.key.node {
426 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
427 _ => return false,
428 };
429 (key == "schema" || key == "output_schema")
430 && schema_type_expr_from_node(&entry.value, scope).is_some()
431 })
432 }
433
434 pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
437 matches!(
438 ty,
439 TypeExpr::Shape(_)
440 | TypeExpr::Applied { .. }
441 | TypeExpr::FnType { .. }
442 | TypeExpr::List(_)
443 | TypeExpr::Iter(_)
444 | TypeExpr::Generator(_)
445 | TypeExpr::Stream(_)
446 | TypeExpr::DictType(_, _)
447 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
448 }
449
450 pub fn check_with_hints(
452 mut self,
453 program: &[SNode],
454 source: &str,
455 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
456 self.source = Some(source.to_string());
457 self.check_inner(program)
458 }
459
460 pub(in crate::typechecker) fn error_at(&mut self, code: Code, message: String, span: Span) {
461 self.diagnostics.push(TypeDiagnostic {
462 code,
463 message,
464 severity: DiagnosticSeverity::Error,
465 span: Some(span),
466 help: None,
467 related: Vec::new(),
468 fix: None,
469 details: None,
470 repair: default_repair(code),
471 });
472 }
473
474 #[allow(dead_code)]
475 pub(in crate::typechecker) fn error_at_with_help(
476 &mut self,
477 code: Code,
478 message: String,
479 span: Span,
480 help: String,
481 ) {
482 self.diagnostics.push(TypeDiagnostic {
483 code,
484 message,
485 severity: DiagnosticSeverity::Error,
486 span: Some(span),
487 help: Some(help),
488 related: Vec::new(),
489 fix: None,
490 details: None,
491 repair: default_repair(code),
492 });
493 }
494
495 pub(in crate::typechecker) fn type_mismatch_at(
496 &mut self,
497 code: Code,
498 context: impl Into<String>,
499 expected: &TypeExpr,
500 actual: &TypeExpr,
501 span: Span,
502 evidence: TypeMismatchEvidence,
503 scope: &TypeScope,
504 ) {
505 let (expected_origin, value_span) = evidence;
506 let nested_mismatch = first_nested_mismatch(expected, actual, scope);
507 let mut message = format!(
508 "{}: expected {}, found {}",
509 context.into(),
510 format_type(expected),
511 format_type(actual)
512 );
513 if let Some(detail) = shape_mismatch_detail(expected, actual)
514 .or_else(|| nested_mismatch.as_ref().map(|note| note.message.clone()))
515 {
516 message.push_str(&format!(" ({detail})"));
517 }
518
519 let mut related = Vec::new();
520 if let Some((span, message)) = expected_origin {
521 related.push(RelatedDiagnostic { span, message });
522 }
523 if let Some(note) = nested_mismatch {
524 related.push(RelatedDiagnostic {
525 span,
526 message: format!("nested mismatch: {}", note.message),
527 });
528 }
529
530 self.diagnostics.push(TypeDiagnostic {
531 code,
532 message,
533 severity: DiagnosticSeverity::Error,
534 span: Some(span),
535 help: coercion_suggestion(expected, actual, value_span, self.source.as_deref()),
536 related,
537 fix: None,
538 details: Some(DiagnosticDetails::TypeMismatch),
539 repair: default_repair(code),
540 });
541 }
542
543 pub(in crate::typechecker) fn error_at_with_fix(
544 &mut self,
545 code: Code,
546 message: String,
547 span: Span,
548 fix: Vec<FixEdit>,
549 ) {
550 self.diagnostics.push(TypeDiagnostic {
551 code,
552 message,
553 severity: DiagnosticSeverity::Error,
554 span: Some(span),
555 help: None,
556 related: Vec::new(),
557 fix: Some(fix),
558 details: None,
559 repair: default_repair(code),
560 });
561 }
562
563 pub(in crate::typechecker) fn exhaustiveness_error_at(
570 &mut self,
571 code: Code,
572 message: String,
573 span: Span,
574 ) {
575 self.diagnostics.push(TypeDiagnostic {
576 code,
577 message,
578 severity: DiagnosticSeverity::Error,
579 span: Some(span),
580 help: None,
581 related: Vec::new(),
582 fix: None,
583 details: None,
584 repair: default_repair(code),
585 });
586 }
587
588 pub(in crate::typechecker) fn exhaustiveness_error_with_missing(
593 &mut self,
594 code: Code,
595 message: String,
596 span: Span,
597 missing: Vec<String>,
598 ) {
599 self.diagnostics.push(TypeDiagnostic {
600 code,
601 message,
602 severity: DiagnosticSeverity::Error,
603 span: Some(span),
604 help: None,
605 related: Vec::new(),
606 fix: None,
607 details: Some(DiagnosticDetails::NonExhaustiveMatch { missing }),
608 repair: default_repair(code),
609 });
610 }
611
612 pub(in crate::typechecker) fn warning_at(&mut self, code: Code, message: String, span: Span) {
613 self.diagnostics.push(TypeDiagnostic {
614 code,
615 message,
616 severity: DiagnosticSeverity::Warning,
617 span: Some(span),
618 help: None,
619 related: Vec::new(),
620 fix: None,
621 details: None,
622 repair: default_repair(code),
623 });
624 }
625
626 #[allow(dead_code)]
627 pub(in crate::typechecker) fn warning_at_with_help(
628 &mut self,
629 code: Code,
630 message: String,
631 span: Span,
632 help: String,
633 ) {
634 self.diagnostics.push(TypeDiagnostic {
635 code,
636 message,
637 severity: DiagnosticSeverity::Warning,
638 span: Some(span),
639 help: Some(help),
640 related: Vec::new(),
641 fix: None,
642 details: None,
643 repair: default_repair(code),
644 });
645 }
646
647 pub(in crate::typechecker) fn lint_warning_at_with_fix(
648 &mut self,
649 code: Code,
650 rule: &'static str,
651 message: String,
652 span: Span,
653 help: String,
654 fix: Vec<FixEdit>,
655 ) {
656 self.diagnostics.push(TypeDiagnostic {
657 code,
658 message,
659 severity: DiagnosticSeverity::Warning,
660 span: Some(span),
661 help: Some(help),
662 related: Vec::new(),
663 fix: Some(fix),
664 details: Some(DiagnosticDetails::LintRule { rule }),
665 repair: default_repair(code),
666 });
667 }
668}
669
670pub(crate) fn default_repair(code: Code) -> Option<Repair> {
675 code.repair_template().map(Repair::from_template)
676}
677
678#[derive(Debug)]
679struct MismatchNote {
680 message: String,
681}
682
683fn first_nested_mismatch(
684 expected: &TypeExpr,
685 actual: &TypeExpr,
686 scope: &TypeScope,
687) -> Option<MismatchNote> {
688 let expected = resolve_type_for_diagnostic(expected, scope);
689 let actual = resolve_type_for_diagnostic(actual, scope);
690 match (&expected, &actual) {
691 (TypeExpr::Shape(expected_fields), TypeExpr::Shape(actual_fields)) => {
692 for expected_field in expected_fields {
693 if expected_field.optional {
694 continue;
695 }
696 let Some(actual_field) = actual_fields
697 .iter()
698 .find(|actual_field| actual_field.name == expected_field.name)
699 else {
700 return Some(MismatchNote {
701 message: format!(
702 "field `{}` is missing; expected {}",
703 expected_field.name,
704 format_type(&expected_field.type_expr)
705 ),
706 });
707 };
708 if !types_compatible_for_diagnostic(
709 &expected_field.type_expr,
710 &actual_field.type_expr,
711 scope,
712 ) {
713 return Some(MismatchNote {
714 message: format!(
715 "field `{}` expected {}, found {}",
716 expected_field.name,
717 format_type(&expected_field.type_expr),
718 format_type(&actual_field.type_expr)
719 ),
720 });
721 }
722 }
723 None
724 }
725 (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
726 if !types_compatible_for_diagnostic(expected_inner, actual_inner, scope)
727 || !types_compatible_for_diagnostic(actual_inner, expected_inner, scope)
728 {
729 Some(MismatchNote {
730 message: format!(
731 "list element expected {}, found {}",
732 format_type(expected_inner),
733 format_type(actual_inner)
734 ),
735 })
736 } else {
737 None
738 }
739 }
740 (
741 TypeExpr::DictType(expected_key, expected_value),
742 TypeExpr::DictType(actual_key, actual_value),
743 ) => {
744 if !types_compatible_for_diagnostic(expected_key, actual_key, scope)
745 || !types_compatible_for_diagnostic(actual_key, expected_key, scope)
746 {
747 Some(MismatchNote {
748 message: format!(
749 "dict key expected {}, found {}",
750 format_type(expected_key),
751 format_type(actual_key)
752 ),
753 })
754 } else if !types_compatible_for_diagnostic(expected_value, actual_value, scope)
755 || !types_compatible_for_diagnostic(actual_value, expected_value, scope)
756 {
757 Some(MismatchNote {
758 message: format!(
759 "dict value expected {}, found {}",
760 format_type(expected_value),
761 format_type(actual_value)
762 ),
763 })
764 } else {
765 None
766 }
767 }
768 (
769 TypeExpr::Applied {
770 name: expected_name,
771 args: expected_args,
772 },
773 TypeExpr::Applied {
774 name: actual_name,
775 args: actual_args,
776 },
777 ) if expected_name == actual_name => expected_args
778 .iter()
779 .zip(actual_args.iter())
780 .enumerate()
781 .find_map(|(idx, (expected_arg, actual_arg))| {
782 if types_compatible_for_diagnostic(expected_arg, actual_arg, scope)
783 && types_compatible_for_diagnostic(actual_arg, expected_arg, scope)
784 {
785 None
786 } else {
787 Some(MismatchNote {
788 message: format!(
789 "{} type argument {} expected {}, found {}",
790 expected_name,
791 idx + 1,
792 format_type(expected_arg),
793 format_type(actual_arg)
794 ),
795 })
796 }
797 }),
798 (
799 TypeExpr::FnType {
800 params: expected_params,
801 return_type: expected_return,
802 },
803 TypeExpr::FnType {
804 params: actual_params,
805 return_type: actual_return,
806 },
807 ) => {
808 for (idx, (expected_param, actual_param)) in
809 expected_params.iter().zip(actual_params.iter()).enumerate()
810 {
811 if !types_compatible_for_diagnostic(actual_param, expected_param, scope) {
812 return Some(MismatchNote {
813 message: format!(
814 "function parameter {} expected {}, found {}",
815 idx + 1,
816 format_type(expected_param),
817 format_type(actual_param)
818 ),
819 });
820 }
821 }
822 if !types_compatible_for_diagnostic(expected_return, actual_return, scope) {
823 Some(MismatchNote {
824 message: format!(
825 "function return expected {}, found {}",
826 format_type(expected_return),
827 format_type(actual_return)
828 ),
829 })
830 } else {
831 None
832 }
833 }
834 _ => None,
835 }
836}
837
838fn types_compatible_for_diagnostic(
839 expected: &TypeExpr,
840 actual: &TypeExpr,
841 scope: &TypeScope,
842) -> bool {
843 TypeChecker::new().types_compatible(expected, actual, scope)
844}
845
846fn resolve_type_for_diagnostic(ty: &TypeExpr, scope: &TypeScope) -> TypeExpr {
847 TypeChecker::new().resolve_alias(ty, scope)
848}
849
850fn coercion_suggestion(
851 expected: &TypeExpr,
852 actual: &TypeExpr,
853 value_span: Option<Span>,
854 source: Option<&str>,
855) -> Option<String> {
856 let expr = value_span
857 .and_then(|span| source.and_then(|source| source.get(span.start..span.end)))
858 .map(str::trim)
859 .filter(|expr| !expr.is_empty());
860 if is_nilable(actual) {
861 return Some("handle `nil` first or provide a default with `??`".to_string());
862 }
863 let expected_ty = expected;
864 let expected = simple_type_name(expected)?;
865 let actual_name = simple_type_name(actual)?;
866 let with_expr = |template: &str| {
867 expr.map(|expr| template.replace("{}", expr))
868 .unwrap_or_else(|| template.replace("{}", "value"))
869 };
870
871 match (expected, actual_name) {
872 ("string", "int" | "float" | "bool" | "nil" | "duration") => {
873 Some(format!("did you mean `{}`?", with_expr("to_string({})")))
874 }
875 ("int", "string") => Some(format!("did you mean `{}`?", with_expr("to_int({})"))),
876 ("float", "string" | "int") => {
877 Some(format!("did you mean `{}`?", with_expr("to_float({})")))
878 }
879 (_, "nil") => Some("handle `nil` first or provide a default with `??`".to_string()),
880 _ if actual_is_result_of(expected_ty, actual) => Some(format!(
881 "did you mean `{}` or `{}`?",
882 with_expr("{}?"),
883 with_expr("unwrap_or({}, default)")
884 )),
885 _ => None,
886 }
887}
888
889fn simple_type_name(ty: &TypeExpr) -> Option<&str> {
890 match ty {
891 TypeExpr::Named(name) => Some(name.as_str()),
892 TypeExpr::LitString(_) => Some("string"),
893 TypeExpr::LitInt(_) => Some("int"),
894 _ => None,
895 }
896}
897
898fn is_nilable(ty: &TypeExpr) -> bool {
899 match ty {
900 TypeExpr::Union(members) if members.len() == 2 => members
901 .iter()
902 .any(|member| matches!(member, TypeExpr::Named(name) if name == "nil")),
903 _ => false,
904 }
905}
906
907fn actual_is_result_of(expected: &TypeExpr, actual: &TypeExpr) -> bool {
908 matches!(
909 actual,
910 TypeExpr::Applied { name, args }
911 if name == "Result" && args.first().is_some_and(|ok| ok == expected)
912 )
913}
914
915pub(in crate::typechecker) fn is_gradual_type_name(name: &str) -> bool {
923 matches!(name, "any" | "unknown" | "_")
924}
925
926impl Default for TypeChecker {
927 fn default() -> Self {
928 Self::new()
929 }
930}
931
932#[cfg(test)]
933mod tests;