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