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