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