1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use smallvec::SmallVec;
4use std::sync::{Arc, OnceLock};
5
6use crate::atomic::Atomic;
7use crate::symbol::Name;
8
9pub fn empty_type_params() -> Arc<[Type]> {
13 static EMPTY: OnceLock<Arc<[Type]>> = OnceLock::new();
14 EMPTY.get_or_init(|| Arc::from([] as [Type; 0])).clone()
15}
16
17pub fn vec_to_type_params(v: Vec<Type>) -> Arc<[Type]> {
20 if v.is_empty() {
21 empty_type_params()
22 } else {
23 Arc::from(v)
24 }
25}
26
27pub type AtomicVec = SmallVec<[Atomic; 2]>;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CloneValidity {
33 Cloneable,
35 Invalid,
37 PossiblyInvalid,
39 Unknown,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub struct Type {
49 pub types: AtomicVec,
50 pub possibly_undefined: bool,
52 pub from_docblock: bool,
54}
55
56impl Type {
57 pub fn empty() -> Self {
60 Self {
61 types: SmallVec::new(),
62 possibly_undefined: false,
63 from_docblock: false,
64 }
65 }
66
67 pub fn single(atomic: Atomic) -> Self {
68 let mut types = SmallVec::new();
69 types.push(atomic);
70 Self {
71 types,
72 possibly_undefined: false,
73 from_docblock: false,
74 }
75 }
76
77 pub fn mixed() -> Self {
78 Self::single(Atomic::TMixed)
79 }
80
81 pub fn void() -> Self {
82 Self::single(Atomic::TVoid)
83 }
84
85 pub fn never() -> Self {
86 Self::single(Atomic::TNever)
87 }
88
89 pub fn null() -> Self {
90 Self::single(Atomic::TNull)
91 }
92
93 pub fn bool() -> Self {
94 Self::single(Atomic::TBool)
95 }
96
97 pub fn int() -> Self {
98 Self::single(Atomic::TInt)
99 }
100
101 pub fn float() -> Self {
102 Self::single(Atomic::TFloat)
103 }
104
105 pub fn string() -> Self {
106 Self::single(Atomic::TString)
107 }
108
109 pub fn nullable(atomic: Atomic) -> Self {
111 let mut types = SmallVec::new();
112 types.push(atomic);
113 types.push(Atomic::TNull);
114 Self {
115 types,
116 possibly_undefined: false,
117 from_docblock: false,
118 }
119 }
120
121 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
123 let mut u = Self::empty();
124 for a in atomics {
125 u.add_type(a);
126 }
127 u
128 }
129
130 pub fn is_empty(&self) -> bool {
133 self.types.is_empty()
134 }
135
136 pub fn is_single(&self) -> bool {
137 self.types.len() == 1
138 }
139
140 pub fn is_nullable(&self) -> bool {
141 self.types.iter().any(|t| matches!(t, Atomic::TNull))
142 }
143
144 pub fn is_mixed(&self) -> bool {
145 self.types.iter().any(|t| match t {
146 Atomic::TMixed => true,
147 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
148 _ => false,
149 })
150 }
151
152 pub fn is_never(&self) -> bool {
153 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
154 }
155
156 pub fn clone_validity(&self) -> CloneValidity {
159 if self.types.is_empty() {
160 return CloneValidity::Unknown;
161 }
162 let mut has_non_object = false;
163 let mut has_other = false; for t in &self.types {
165 match t {
166 Atomic::TTemplateParam { as_type, .. } => match as_type.clone_validity() {
167 CloneValidity::Invalid => has_non_object = true,
168 CloneValidity::PossiblyInvalid => {
169 has_non_object = true;
170 has_other = true;
171 }
172 CloneValidity::Cloneable | CloneValidity::Unknown => has_other = true,
173 },
174 other if other.is_definitely_non_object() => has_non_object = true,
175 _ => has_other = true,
176 }
177 }
178 match (has_non_object, has_other) {
179 (true, false) => CloneValidity::Invalid,
180 (true, true) => CloneValidity::PossiblyInvalid,
181 _ => CloneValidity::Cloneable,
182 }
183 }
184
185 pub fn is_void(&self) -> bool {
186 self.is_single() && matches!(self.types[0], Atomic::TVoid)
187 }
188
189 pub fn can_be_falsy(&self) -> bool {
190 self.types.iter().any(|t| t.can_be_falsy())
191 }
192
193 pub fn can_be_truthy(&self) -> bool {
194 self.types.iter().any(|t| t.can_be_truthy())
195 }
196
197 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
198 self.types.iter().any(f)
199 }
200
201 pub fn has_named_object(&self, fqcn: &str) -> bool {
202 self.types.iter().any(|t| match t {
203 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
204 _ => false,
205 })
206 }
207
208 pub fn add_type(&mut self, atomic: Atomic) {
213 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
215 return;
216 }
217
218 if matches!(atomic, Atomic::TMixed) {
220 self.types.clear();
221 self.types.push(Atomic::TMixed);
222 return;
223 }
224
225 let atomic = if let Atomic::TConditional {
228 param_name: _,
229 subject: _,
230 if_true,
231 if_false,
232 } = &atomic
233 {
234 let mut simplified_true = Type::empty();
235 for t in &if_true.types {
236 simplified_true.add_type(t.clone());
237 }
238 let mut simplified_false = Type::empty();
239 for t in &if_false.types {
240 simplified_false.add_type(t.clone());
241 }
242 if simplified_true == simplified_false {
243 for t in simplified_true.types {
244 self.add_type(t);
245 }
246 return;
247 }
248 atomic
249 } else {
250 atomic
251 };
252
253 if self.types.contains(&atomic) {
255 return;
256 }
257
258 if let Atomic::TLiteralInt(_) = &atomic {
260 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
261 return;
262 }
263 }
264 if let Atomic::TLiteralString(_) = &atomic {
266 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
267 return;
268 }
269 }
270 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
272 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
273 {
274 return;
275 }
276 if matches!(atomic, Atomic::TInt) {
278 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
279 }
280 if matches!(atomic, Atomic::TString) {
282 self.types
283 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
284 }
285 if matches!(atomic, Atomic::TBool) {
287 self.types
288 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
289 }
290
291 if matches!(atomic, Atomic::TNever) {
293 if !self.types.is_empty() {
294 return;
295 }
296 } else {
297 self.types.retain(|t| !matches!(t, Atomic::TNever));
298 }
299
300 if let Atomic::TKeyedArray { properties, .. } = &atomic {
303 if properties.is_empty() {
304 for existing in &self.types {
305 match existing {
306 Atomic::TArray { .. }
307 | Atomic::TNonEmptyArray { .. }
308 | Atomic::TList { .. }
309 | Atomic::TNonEmptyList { .. } => {
310 return; }
312 _ => {}
313 }
314 }
315 }
316 }
317
318 let is_generic_array_or_list = matches!(
320 &atomic,
321 Atomic::TArray { .. }
322 | Atomic::TNonEmptyArray { .. }
323 | Atomic::TList { .. }
324 | Atomic::TNonEmptyList { .. }
325 );
326 if is_generic_array_or_list {
327 self.types.retain(|t| {
328 if let Atomic::TKeyedArray { properties, .. } = t {
329 !properties.is_empty()
330 } else {
331 true
332 }
333 });
334 }
335
336 self.types.push(atomic);
337 }
338
339 pub fn remove_null(&self) -> Type {
343 self.filter(|t| !matches!(t, Atomic::TNull))
344 }
345
346 pub fn remove_false(&self) -> Type {
348 self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
349 }
350
351 pub fn core_type(&self) -> Type {
353 self.remove_null().remove_false()
354 }
355
356 pub fn narrow_to_truthy(&self) -> Type {
358 if self.is_mixed() {
359 return Type::mixed();
360 }
361 let narrowed = self.filter(|t| t.can_be_truthy());
362 narrowed.filter(|t| match t {
364 Atomic::TLiteralInt(0) => false,
365 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
366 Atomic::TLiteralFloat(0, 0) => false,
367 _ => true,
368 })
369 }
370
371 pub fn narrow_to_falsy(&self) -> Type {
373 if self.is_mixed() {
374 return Type::from_vec(vec![
375 Atomic::TNull,
376 Atomic::TFalse,
377 Atomic::TLiteralInt(0),
378 Atomic::TLiteralString("".into()),
379 ]);
380 }
381 self.filter(|t| t.can_be_falsy())
382 }
383
384 pub fn narrow_instanceof(&self, class: &str) -> Type {
390 let narrowed_ty = Atomic::TNamedObject {
391 fqcn: class.into(),
392 type_params: empty_type_params(),
393 };
394 let has_object = self.types.iter().any(|t| {
396 matches!(
397 t,
398 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
400 });
401 if has_object || self.is_empty() {
402 Type::single(narrowed_ty)
403 } else {
404 Type::single(narrowed_ty)
407 }
408 }
409
410 pub fn narrow_to_string(&self) -> Type {
412 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
413 }
414
415 pub fn narrow_to_int(&self) -> Type {
417 self.filter(|t| {
418 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
419 })
420 }
421
422 pub fn narrow_to_float(&self) -> Type {
424 self.filter(|t| {
425 matches!(
426 t,
427 Atomic::TFloat
428 | Atomic::TLiteralFloat(..)
429 | Atomic::TMixed
430 | Atomic::TScalar
431 | Atomic::TNumeric
432 )
433 })
434 }
435
436 pub fn narrow_to_bool(&self) -> Type {
438 self.filter(|t| {
439 matches!(
440 t,
441 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
442 )
443 })
444 }
445
446 pub fn narrow_to_null(&self) -> Type {
448 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
449 }
450
451 pub fn narrow_to_array(&self) -> Type {
453 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
454 }
455
456 pub fn narrow_to_object(&self) -> Type {
461 let mut out = Type::empty();
462 for t in &self.types {
463 if matches!(t, Atomic::TMixed) {
464 out.add_type(Atomic::TObject);
465 } else if t.is_object() {
466 out.add_type(t.clone());
467 }
468 }
469 if out.types.is_empty() {
470 self.filter(|t| t.is_object())
471 } else {
472 out
473 }
474 }
475
476 pub fn narrow_to_callable(&self) -> Type {
478 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
479 }
480
481 pub fn narrow_to_scalar(&self) -> Type {
483 self.filter(|t| {
484 matches!(
485 t,
486 Atomic::TString
487 | Atomic::TLiteralString(..)
488 | Atomic::TNumericString
489 | Atomic::TInt
490 | Atomic::TLiteralInt(..)
491 | Atomic::TFloat
492 | Atomic::TLiteralFloat(..)
493 | Atomic::TBool
494 | Atomic::TTrue
495 | Atomic::TFalse
496 | Atomic::TScalar
497 | Atomic::TMixed
498 )
499 })
500 }
501
502 pub fn narrow_to_iterable(&self) -> Type {
505 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
506 }
507
508 pub fn narrow_to_countable(&self) -> Type {
511 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
512 }
513
514 pub fn narrow_to_resource(&self) -> Type {
518 self.filter(|t| matches!(t, Atomic::TMixed))
520 }
521
522 pub fn merge(a: &Type, b: &Type) -> Type {
527 if b.types.is_empty() {
529 let mut result = a.clone();
530 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
531 return result;
532 }
533 if a.types.is_empty() {
535 let mut result = b.clone();
536 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
537 return result;
538 }
539 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
541 let mut result = a.clone();
542 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
543 return result;
544 }
545 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
547 return Type {
548 types: smallvec::smallvec![Atomic::TMixed],
549 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
550 from_docblock: a.from_docblock || b.from_docblock,
551 };
552 }
553 let mut result = a.clone();
554 result.merge_with(b);
555 result
556 }
557
558 pub fn merge_with(&mut self, other: &Type) {
560 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
561 self.possibly_undefined |= other.possibly_undefined;
562 return;
563 }
564 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
565 self.types.clear();
566 self.types.push(Atomic::TMixed);
567 self.possibly_undefined |= other.possibly_undefined;
568 return;
569 }
570 for atomic in &other.types {
571 self.add_type(atomic.clone());
572 }
573 self.possibly_undefined |= other.possibly_undefined;
574 }
575
576 pub fn intersect_with(&self, other: &Type) -> Type {
580 if self.is_mixed() {
581 return other.clone();
582 }
583 if other.is_mixed() {
584 return self.clone();
585 }
586 let mut result = Type::empty();
588 for a in &self.types {
589 for b in &other.types {
590 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
591 result.add_type(a.clone());
592 break;
593 }
594 }
595 }
596 if result.is_empty() {
597 Type::never()
598 } else {
599 result
600 }
601 }
602
603 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
607 if bindings.is_empty() {
608 return self.clone();
609 }
610 let mut result = Type::empty();
611 result.possibly_undefined = self.possibly_undefined;
612 result.from_docblock = self.from_docblock;
613 for atomic in &self.types {
614 match atomic {
615 Atomic::TTemplateParam { name, .. } => {
616 if let Some(resolved) = bindings.get(name) {
617 for t in &resolved.types {
618 result.add_type(t.clone());
619 }
620 } else {
621 result.add_type(atomic.clone());
622 }
623 }
624 Atomic::TArray { key, value } => {
625 result.add_type(Atomic::TArray {
626 key: Box::new(key.substitute_templates(bindings)),
627 value: Box::new(value.substitute_templates(bindings)),
628 });
629 }
630 Atomic::TList { value } => {
631 result.add_type(Atomic::TList {
632 value: Box::new(value.substitute_templates(bindings)),
633 });
634 }
635 Atomic::TNonEmptyArray { key, value } => {
636 result.add_type(Atomic::TNonEmptyArray {
637 key: Box::new(key.substitute_templates(bindings)),
638 value: Box::new(value.substitute_templates(bindings)),
639 });
640 }
641 Atomic::TNonEmptyList { value } => {
642 result.add_type(Atomic::TNonEmptyList {
643 value: Box::new(value.substitute_templates(bindings)),
644 });
645 }
646 Atomic::TKeyedArray {
647 properties,
648 is_open,
649 is_list,
650 } => {
651 use crate::atomic::KeyedProperty;
652 let new_props = properties
653 .iter()
654 .map(|(k, prop)| {
655 (
656 k.clone(),
657 KeyedProperty {
658 ty: prop.ty.substitute_templates(bindings),
659 optional: prop.optional,
660 },
661 )
662 })
663 .collect();
664 result.add_type(Atomic::TKeyedArray {
665 properties: new_props,
666 is_open: *is_open,
667 is_list: *is_list,
668 });
669 }
670 Atomic::TCallable {
671 params,
672 return_type,
673 } => {
674 result.add_type(Atomic::TCallable {
675 params: params.as_ref().map(|ps| {
676 ps.iter()
677 .map(|p| substitute_in_fn_param(p, bindings))
678 .collect()
679 }),
680 return_type: return_type
681 .as_ref()
682 .map(|r| Box::new(r.substitute_templates(bindings))),
683 });
684 }
685 Atomic::TClosure {
686 params,
687 return_type,
688 this_type,
689 } => {
690 result.add_type(Atomic::TClosure {
691 params: params
692 .iter()
693 .map(|p| substitute_in_fn_param(p, bindings))
694 .collect(),
695 return_type: Box::new(return_type.substitute_templates(bindings)),
696 this_type: this_type
697 .as_ref()
698 .map(|t| Box::new(t.substitute_templates(bindings))),
699 });
700 }
701 Atomic::TConditional {
702 param_name,
703 subject,
704 if_true,
705 if_false,
706 } => {
707 let new_subject = subject.substitute_templates(bindings);
708 let new_if_true = if_true.substitute_templates(bindings);
709 let new_if_false = if_false.substitute_templates(bindings);
710
711 let resolved = if let Some(name) = param_name {
715 if let Some(bound) = bindings.get(name) {
716 if new_subject.types.len() == 1 {
717 resolve_conditional_branch(
718 &new_subject.types[0],
719 bound,
720 &new_if_true,
721 &new_if_false,
722 )
723 } else {
724 None
725 }
726 } else {
727 None
728 }
729 } else {
730 None
731 };
732
733 if let Some(branch) = resolved {
734 for t in branch.types {
735 result.add_type(t);
736 }
737 } else {
738 result.add_type(Atomic::TConditional {
739 param_name: *param_name,
740 subject: Box::new(new_subject),
741 if_true: Box::new(new_if_true),
742 if_false: Box::new(new_if_false),
743 });
744 }
745 }
746 Atomic::TIntersection { parts } => {
747 result.add_type(Atomic::TIntersection {
748 parts: vec_to_type_params(
749 parts
750 .iter()
751 .map(|p| p.substitute_templates(bindings))
752 .collect(),
753 ),
754 });
755 }
756 Atomic::TNamedObject { fqcn, type_params } => {
757 if type_params.is_empty() && !fqcn.contains('\\') {
764 if let Some(resolved) = bindings.get(fqcn) {
765 for t in &resolved.types {
766 result.add_type(t.clone());
767 }
768 continue;
769 }
770 }
771 let new_params: Vec<Type> = type_params
772 .iter()
773 .map(|p| p.substitute_templates(bindings))
774 .collect();
775 result.add_type(Atomic::TNamedObject {
776 fqcn: *fqcn,
777 type_params: vec_to_type_params(new_params),
778 });
779 }
780 Atomic::TClassString(Some(param_name)) => {
782 if let Some(resolved) = bindings.get(param_name) {
783 for r_atomic in &resolved.types {
784 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
785 Some(*fqcn)
786 } else {
787 None
788 };
789 result.add_type(Atomic::TClassString(cls_name));
790 }
791 } else {
792 result.add_type(atomic.clone());
793 }
794 }
795 _ => {
796 result.add_type(atomic.clone());
797 }
798 }
799 }
800 result
801 }
802
803 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
809 where
810 F: Fn(&str) -> Option<Type>,
811 {
812 self.resolve_conditional_inner(&lookup)
813 }
814
815 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
816 where
817 F: Fn(&str) -> Option<Type>,
818 {
819 let mut result = Type::empty();
820 for atomic in self.types {
821 match atomic {
822 Atomic::TConditional {
823 ref param_name,
824 ref subject,
825 ref if_true,
826 ref if_false,
827 } => {
828 let resolved = if subject.types.len() == 1 {
829 if let Some(name) = param_name {
830 if let Some(arg_ty) = lookup(name.as_ref()) {
831 resolve_conditional_branch(
832 &subject.types[0],
833 &arg_ty,
834 if_true,
835 if_false,
836 )
837 } else {
838 None
839 }
840 } else {
841 None
842 }
843 } else {
844 None
845 };
846
847 if let Some(branch) = resolved {
848 for t in branch.resolve_conditional_inner(lookup).types {
850 result.add_type(t);
851 }
852 } else {
853 for t in if_true.clone().resolve_conditional_inner(lookup).types {
856 result.add_type(t);
857 }
858 for t in if_false.clone().resolve_conditional_inner(lookup).types {
859 result.add_type(t);
860 }
861 }
862 }
863 other => result.add_type(other),
864 }
865 }
866 result
867 }
868
869 pub fn is_subtype_structural(&self, other: &Type) -> bool {
879 if other.is_mixed() {
880 return true;
881 }
882 if self.is_never() {
883 return true; }
885 self.types
886 .iter()
887 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
888 }
889
890 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
893 let mut result = Type::empty();
894 result.possibly_undefined = self.possibly_undefined;
895 result.from_docblock = self.from_docblock;
896 for atomic in &self.types {
897 if f(atomic) {
898 result.types.push(atomic.clone());
899 }
900 }
901 result
902 }
903
904 pub fn possibly_undefined(mut self) -> Self {
906 self.possibly_undefined = true;
907 self
908 }
909
910 pub fn from_docblock(mut self) -> Self {
912 self.from_docblock = true;
913 self
914 }
915}
916
917fn is_string_atomic(a: &Atomic) -> bool {
922 matches!(
923 a,
924 Atomic::TString
925 | Atomic::TNonEmptyString
926 | Atomic::TLiteralString(_)
927 | Atomic::TNumericString
928 | Atomic::TClassString(_)
929 | Atomic::TCallableString
930 )
931}
932
933fn is_array_atomic(a: &Atomic) -> bool {
934 matches!(
935 a,
936 Atomic::TArray { .. }
937 | Atomic::TNonEmptyArray { .. }
938 | Atomic::TKeyedArray { .. }
939 | Atomic::TList { .. }
940 | Atomic::TNonEmptyList { .. }
941 )
942}
943
944fn is_list_atomic(a: &Atomic) -> bool {
945 match a {
946 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
947 Atomic::TKeyedArray { is_list, .. } => *is_list,
948 _ => false,
949 }
950}
951
952fn resolve_conditional_branch(
958 subject: &Atomic,
959 arg_ty: &Type,
960 if_true: &Type,
961 if_false: &Type,
962) -> Option<Type> {
963 let predicate: fn(&Atomic) -> bool = match subject {
964 Atomic::TNull => |a| matches!(a, Atomic::TNull),
965 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
966 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
967 Atomic::TString => is_string_atomic,
968 Atomic::TList { .. } => is_list_atomic,
969 Atomic::TArray { .. } => is_array_atomic,
970 _ => return None,
971 };
972
973 if arg_ty.types.is_empty() {
974 return None;
975 }
976 let all_match = arg_ty.types.iter().all(&predicate);
977 let none_match = !arg_ty.types.iter().any(predicate);
978 if all_match {
979 Some(if_true.clone())
980 } else if none_match {
981 Some(if_false.clone())
982 } else {
983 None
984 }
985}
986
987fn substitute_in_fn_param(
992 p: &crate::atomic::FnParam,
993 bindings: &FxHashMap<Name, Type>,
994) -> crate::atomic::FnParam {
995 crate::atomic::FnParam {
996 name: p.name,
997 ty: p.ty.as_ref().map(|t| {
998 let u = t.to_union();
999 let substituted = u.substitute_templates(bindings);
1000 crate::compact::SimpleType::from_union(substituted)
1001 }),
1002 default: p.default.as_ref().map(|d| {
1003 let u = d.to_union();
1004 let substituted = u.substitute_templates(bindings);
1005 crate::compact::SimpleType::from_union(substituted)
1006 }),
1007 is_variadic: p.is_variadic,
1008 is_byref: p.is_byref,
1009 is_optional: p.is_optional,
1010 }
1011}
1012
1013fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1018 if sub == sup {
1019 return true;
1020 }
1021 match (sub, sup) {
1022 (Atomic::TNever, _) => true,
1024 (_, Atomic::TMixed) => true,
1026 (Atomic::TMixed, _) => true,
1027 (_, Atomic::TTemplateParam { as_type, .. }) => {
1032 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1033 }
1034
1035 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1037 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1038 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1039 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1040 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1041 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1042 (Atomic::TPositiveInt, Atomic::TInt) => true,
1043 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1044 (Atomic::TPositiveInt, Atomic::TNumeric) => true,
1045 (Atomic::TPositiveInt, Atomic::TScalar) => true,
1046 (Atomic::TNegativeInt, Atomic::TInt) => true,
1047 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1048 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1049 (Atomic::TIntRange { .. }, Atomic::TNumeric) => true,
1050 (Atomic::TIntRange { .. }, Atomic::TScalar) => true,
1051 (Atomic::TPositiveInt, Atomic::TIntRange { min, max }) => {
1053 max.is_none() && min.is_none_or(|m| m <= 1)
1054 }
1055 (
1057 Atomic::TIntRange {
1058 min: sub_min,
1059 max: sub_max,
1060 },
1061 Atomic::TIntRange {
1062 min: sup_min,
1063 max: sup_max,
1064 },
1065 ) => {
1066 let lower_ok = match (sub_min, sup_min) {
1067 (_, None) => true,
1068 (None, Some(_)) => false,
1069 (Some(sl), Some(su)) => sl >= su,
1070 };
1071 let upper_ok = match (sub_max, sup_max) {
1072 (None, None) | (Some(_), None) => true,
1073 (None, Some(_)) => false,
1074 (Some(sl), Some(su)) => sl <= su,
1075 };
1076 lower_ok && upper_ok
1077 }
1078
1079 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1080 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1081 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1082
1083 (Atomic::TLiteralString(s), Atomic::TString) => {
1084 let _ = s;
1085 true
1086 }
1087 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1088 let _ = s;
1089 true
1090 }
1091 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1092 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1093 (Atomic::TLiteralString(_), Atomic::TClassString(_)) => true,
1096 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1097 (Atomic::TNonEmptyString, Atomic::TString) => true,
1098 (Atomic::TCallableString, Atomic::TString) => true,
1099 (Atomic::TNumericString, Atomic::TString) => true,
1100 (Atomic::TClassString(_), Atomic::TString) => true,
1101 (Atomic::TInterfaceString, Atomic::TString) => true,
1102 (Atomic::TEnumString, Atomic::TString) => true,
1103 (Atomic::TTraitString, Atomic::TString) => true,
1104
1105 (Atomic::TTrue, Atomic::TBool) => true,
1106 (Atomic::TFalse, Atomic::TBool) => true,
1107
1108 (Atomic::TInt, Atomic::TNumeric) => true,
1109 (Atomic::TFloat, Atomic::TNumeric) => true,
1110 (Atomic::TNumericString, Atomic::TNumeric) => true,
1111
1112 (Atomic::TInt, Atomic::TScalar) => true,
1113 (Atomic::TFloat, Atomic::TScalar) => true,
1114 (Atomic::TString, Atomic::TScalar) => true,
1115 (Atomic::TBool, Atomic::TScalar) => true,
1116 (Atomic::TNumeric, Atomic::TScalar) => true,
1117 (Atomic::TTrue, Atomic::TScalar) => true,
1118 (Atomic::TFalse, Atomic::TScalar) => true,
1119
1120 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1122 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1123 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1124 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1126 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1127 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1129 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1130 (
1134 Atomic::TNamedObject {
1135 fqcn: sub_fqcn,
1136 type_params: sub_params,
1137 },
1138 Atomic::TNamedObject {
1139 fqcn: sup_fqcn,
1140 type_params: sup_params,
1141 },
1142 ) => sub_fqcn == sup_fqcn && (sup_params.is_empty() || sub_params == sup_params),
1143
1144 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1146 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1147 (Atomic::TInt, Atomic::TFloat) => true,
1148
1149 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
1151
1152 (Atomic::TString, Atomic::TCallable { .. }) => true,
1154 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1155 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1156 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1157 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1158 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1159
1160 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1162 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1164 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
1166 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1168 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1170 fqcn.as_ref().eq_ignore_ascii_case("closure")
1171 }
1172 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1173 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1175 fqcn.as_ref().eq_ignore_ascii_case("closure")
1176 }
1177 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1179 fqcn.as_ref().eq_ignore_ascii_case("closure")
1180 }
1181
1182 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1184 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1185 }
1186 (Atomic::TNonEmptyList { value }, Atomic::TArray { key, value: av }) => {
1187 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1188 }
1189 (Atomic::TNonEmptyList { value }, Atomic::TNonEmptyArray { key, value: av }) => {
1190 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1191 }
1192 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1193 value.is_subtype_structural(lv)
1194 }
1195 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1197 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1198 && av.is_subtype_structural(lv)
1199 }
1200 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1201 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1202 && av.is_subtype_structural(lv)
1203 }
1204 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1205 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1206 && av.is_subtype_structural(lv)
1207 }
1208 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1209 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1210 && av.is_subtype_structural(lv)
1211 }
1212 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1214 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1215 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1216 }
1217
1218 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1220 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1221 }
1222
1223 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
1225
1226 (
1228 Atomic::TKeyedArray {
1229 properties,
1230 is_list,
1231 ..
1232 },
1233 Atomic::TList { value: lv },
1234 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1235 (
1236 Atomic::TKeyedArray {
1237 properties,
1238 is_list,
1239 ..
1240 },
1241 Atomic::TNonEmptyList { value: lv },
1242 ) => {
1243 *is_list
1244 && !properties.is_empty()
1245 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1246 }
1247
1248 _ => false,
1249 }
1250}
1251
1252#[cfg(test)]
1257mod tests {
1258 use std::sync::Arc;
1259
1260 use super::*;
1261
1262 #[test]
1263 fn single_is_single() {
1264 let u = Type::single(Atomic::TString);
1265 assert!(u.is_single());
1266 assert!(!u.is_nullable());
1267 }
1268
1269 #[test]
1270 fn nullable_has_null() {
1271 let u = Type::nullable(Atomic::TString);
1272 assert!(u.is_nullable());
1273 assert_eq!(u.types.len(), 2);
1274 }
1275
1276 #[test]
1277 fn add_type_deduplicates() {
1278 let mut u = Type::single(Atomic::TString);
1279 u.add_type(Atomic::TString);
1280 assert_eq!(u.types.len(), 1);
1281 }
1282
1283 #[test]
1284 fn add_type_literal_subsumed_by_base() {
1285 let mut u = Type::single(Atomic::TInt);
1286 u.add_type(Atomic::TLiteralInt(42));
1287 assert_eq!(u.types.len(), 1);
1288 assert!(matches!(u.types[0], Atomic::TInt));
1289 }
1290
1291 #[test]
1292 fn add_type_base_widens_literals() {
1293 let mut u = Type::single(Atomic::TLiteralInt(1));
1294 u.add_type(Atomic::TLiteralInt(2));
1295 u.add_type(Atomic::TInt);
1296 assert_eq!(u.types.len(), 1);
1297 assert!(matches!(u.types[0], Atomic::TInt));
1298 }
1299
1300 #[test]
1301 fn mixed_subsumes_everything() {
1302 let mut u = Type::single(Atomic::TString);
1303 u.add_type(Atomic::TMixed);
1304 assert_eq!(u.types.len(), 1);
1305 assert!(u.is_mixed());
1306 }
1307
1308 #[test]
1309 fn remove_null() {
1310 let u = Type::nullable(Atomic::TString);
1311 let narrowed = u.remove_null();
1312 assert!(!narrowed.is_nullable());
1313 assert_eq!(narrowed.types.len(), 1);
1314 }
1315
1316 #[test]
1317 fn narrow_to_truthy_removes_null_false() {
1318 let mut u = Type::empty();
1319 u.add_type(Atomic::TString);
1320 u.add_type(Atomic::TNull);
1321 u.add_type(Atomic::TFalse);
1322 let truthy = u.narrow_to_truthy();
1323 assert!(!truthy.is_nullable());
1324 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1325 }
1326
1327 #[test]
1328 fn merge_combines_types() {
1329 let a = Type::single(Atomic::TString);
1330 let b = Type::single(Atomic::TInt);
1331 let merged = Type::merge(&a, &b);
1332 assert_eq!(merged.types.len(), 2);
1333 }
1334
1335 #[test]
1336 fn subtype_literal_int_under_int() {
1337 let sub = Type::single(Atomic::TLiteralInt(5));
1338 let sup = Type::single(Atomic::TInt);
1339 assert!(sub.is_subtype_structural(&sup));
1340 }
1341
1342 #[test]
1343 fn subtype_never_is_bottom() {
1344 let never = Type::never();
1345 let string = Type::single(Atomic::TString);
1346 assert!(never.is_subtype_structural(&string));
1347 }
1348
1349 #[test]
1350 fn subtype_everything_under_mixed() {
1351 let string = Type::single(Atomic::TString);
1352 let mixed = Type::mixed();
1353 assert!(string.is_subtype_structural(&mixed));
1354 }
1355
1356 #[test]
1357 fn template_substitution() {
1358 let mut bindings = FxHashMap::default();
1359 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1360
1361 let tmpl = Type::single(Atomic::TTemplateParam {
1362 name: Name::new("T"),
1363 as_type: Box::new(Type::mixed()),
1364 defining_entity: Name::new("MyClass"),
1365 });
1366
1367 let resolved = tmpl.substitute_templates(&bindings);
1368 assert_eq!(resolved.types.len(), 1);
1369 assert!(matches!(resolved.types[0], Atomic::TString));
1370 }
1371
1372 #[test]
1373 fn intersection_is_object() {
1374 let parts = vec![
1375 Type::single(Atomic::TNamedObject {
1376 fqcn: Name::new("Iterator"),
1377 type_params: empty_type_params(),
1378 }),
1379 Type::single(Atomic::TNamedObject {
1380 fqcn: Name::new("Countable"),
1381 type_params: empty_type_params(),
1382 }),
1383 ];
1384 let atomic = Atomic::TIntersection {
1385 parts: vec_to_type_params(parts),
1386 };
1387 assert!(atomic.is_object());
1388 assert!(!atomic.can_be_falsy());
1389 assert!(atomic.can_be_truthy());
1390 }
1391
1392 #[test]
1393 fn intersection_display_two_parts() {
1394 let parts = vec![
1395 Type::single(Atomic::TNamedObject {
1396 fqcn: Name::new("Iterator"),
1397 type_params: empty_type_params(),
1398 }),
1399 Type::single(Atomic::TNamedObject {
1400 fqcn: Name::new("Countable"),
1401 type_params: empty_type_params(),
1402 }),
1403 ];
1404 let u = Type::single(Atomic::TIntersection {
1405 parts: vec_to_type_params(parts),
1406 });
1407 assert_eq!(format!("{u}"), "Iterator&Countable");
1408 }
1409
1410 #[test]
1411 fn intersection_display_three_parts() {
1412 let parts = vec![
1413 Type::single(Atomic::TNamedObject {
1414 fqcn: Name::new("A"),
1415 type_params: empty_type_params(),
1416 }),
1417 Type::single(Atomic::TNamedObject {
1418 fqcn: Name::new("B"),
1419 type_params: empty_type_params(),
1420 }),
1421 Type::single(Atomic::TNamedObject {
1422 fqcn: Name::new("C"),
1423 type_params: empty_type_params(),
1424 }),
1425 ];
1426 let u = Type::single(Atomic::TIntersection {
1427 parts: vec_to_type_params(parts),
1428 });
1429 assert_eq!(format!("{u}"), "A&B&C");
1430 }
1431
1432 #[test]
1433 fn intersection_in_nullable_union_display() {
1434 let intersection = Atomic::TIntersection {
1435 parts: vec_to_type_params(vec![
1436 Type::single(Atomic::TNamedObject {
1437 fqcn: Name::new("Iterator"),
1438 type_params: empty_type_params(),
1439 }),
1440 Type::single(Atomic::TNamedObject {
1441 fqcn: Name::new("Countable"),
1442 type_params: empty_type_params(),
1443 }),
1444 ]),
1445 };
1446 let mut u = Type::single(intersection);
1447 u.add_type(Atomic::TNull);
1448 assert!(u.is_nullable());
1449 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1450 }
1451
1452 fn t_param(name: &str) -> Type {
1455 Type::single(Atomic::TTemplateParam {
1456 name: Name::new(name),
1457 as_type: Box::new(Type::mixed()),
1458 defining_entity: Name::new("Fn"),
1459 })
1460 }
1461
1462 fn bindings_t_string() -> FxHashMap<Name, Type> {
1463 let mut b = FxHashMap::default();
1464 b.insert(Name::new("T"), Type::single(Atomic::TString));
1465 b
1466 }
1467
1468 #[test]
1469 fn substitute_non_empty_array_key_and_value() {
1470 let ty = Type::single(Atomic::TNonEmptyArray {
1471 key: Box::new(t_param("T")),
1472 value: Box::new(t_param("T")),
1473 });
1474 let result = ty.substitute_templates(&bindings_t_string());
1475 assert_eq!(result.types.len(), 1);
1476 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1477 panic!("expected TNonEmptyArray");
1478 };
1479 assert!(matches!(key.types[0], Atomic::TString));
1480 assert!(matches!(value.types[0], Atomic::TString));
1481 }
1482
1483 #[test]
1484 fn substitute_non_empty_list_value() {
1485 let ty = Type::single(Atomic::TNonEmptyList {
1486 value: Box::new(t_param("T")),
1487 });
1488 let result = ty.substitute_templates(&bindings_t_string());
1489 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1490 panic!("expected TNonEmptyList");
1491 };
1492 assert!(matches!(value.types[0], Atomic::TString));
1493 }
1494
1495 #[test]
1496 fn substitute_keyed_array_property_types() {
1497 use crate::atomic::{ArrayKey, KeyedProperty};
1498 use indexmap::IndexMap;
1499 let mut props = IndexMap::new();
1500 props.insert(
1501 ArrayKey::String(Arc::from("name")),
1502 KeyedProperty {
1503 ty: t_param("T"),
1504 optional: false,
1505 },
1506 );
1507 props.insert(
1508 ArrayKey::String(Arc::from("tag")),
1509 KeyedProperty {
1510 ty: t_param("T"),
1511 optional: true,
1512 },
1513 );
1514 let ty = Type::single(Atomic::TKeyedArray {
1515 properties: props,
1516 is_open: true,
1517 is_list: false,
1518 });
1519 let result = ty.substitute_templates(&bindings_t_string());
1520 let Atomic::TKeyedArray {
1521 properties,
1522 is_open,
1523 is_list,
1524 } = &result.types[0]
1525 else {
1526 panic!("expected TKeyedArray");
1527 };
1528 assert!(is_open);
1529 assert!(!is_list);
1530 assert!(matches!(
1531 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1532 Atomic::TString
1533 ));
1534 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1535 assert!(matches!(
1536 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1537 Atomic::TString
1538 ));
1539 }
1540
1541 #[test]
1542 fn substitute_callable_params_and_return() {
1543 use crate::atomic::FnParam;
1544 let ty = Type::single(Atomic::TCallable {
1545 params: Some(vec![FnParam {
1546 name: Name::new("x"),
1547 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1548 default: None,
1549 is_variadic: false,
1550 is_byref: false,
1551 is_optional: false,
1552 }]),
1553 return_type: Some(Box::new(t_param("T"))),
1554 });
1555 let result = ty.substitute_templates(&bindings_t_string());
1556 let Atomic::TCallable {
1557 params,
1558 return_type,
1559 } = &result.types[0]
1560 else {
1561 panic!("expected TCallable");
1562 };
1563 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1564 let param_union = param_ty.to_union();
1565 assert!(matches!(param_union.types[0], Atomic::TString));
1566 let ret = return_type.as_ref().unwrap();
1567 assert!(matches!(ret.types[0], Atomic::TString));
1568 }
1569
1570 #[test]
1571 fn substitute_callable_bare_no_panic() {
1572 let ty = Type::single(Atomic::TCallable {
1574 params: None,
1575 return_type: None,
1576 });
1577 let result = ty.substitute_templates(&bindings_t_string());
1578 assert!(matches!(
1579 result.types[0],
1580 Atomic::TCallable {
1581 params: None,
1582 return_type: None
1583 }
1584 ));
1585 }
1586
1587 #[test]
1588 fn substitute_closure_params_return_and_this() {
1589 use crate::atomic::FnParam;
1590 let ty = Type::single(Atomic::TClosure {
1591 params: vec![FnParam {
1592 name: Name::new("a"),
1593 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1594 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1595 is_variadic: true,
1596 is_byref: true,
1597 is_optional: true,
1598 }],
1599 return_type: Box::new(t_param("T")),
1600 this_type: Some(Box::new(t_param("T"))),
1601 });
1602 let result = ty.substitute_templates(&bindings_t_string());
1603 let Atomic::TClosure {
1604 params,
1605 return_type,
1606 this_type,
1607 } = &result.types[0]
1608 else {
1609 panic!("expected TClosure");
1610 };
1611 let p = ¶ms[0];
1612 let ty_union = p.ty.as_ref().unwrap().to_union();
1613 let default_union = p.default.as_ref().unwrap().to_union();
1614 assert!(matches!(ty_union.types[0], Atomic::TString));
1615 assert!(matches!(default_union.types[0], Atomic::TString));
1616 assert!(p.is_variadic);
1618 assert!(p.is_byref);
1619 assert!(p.is_optional);
1620 assert!(matches!(return_type.types[0], Atomic::TString));
1621 assert!(matches!(
1622 this_type.as_ref().unwrap().types[0],
1623 Atomic::TString
1624 ));
1625 }
1626
1627 #[test]
1628 fn substitute_conditional_all_branches() {
1629 let ty = Type::single(Atomic::TConditional {
1630 param_name: None,
1631 subject: Box::new(t_param("T")),
1632 if_true: Box::new(t_param("T")),
1633 if_false: Box::new(Type::single(Atomic::TInt)),
1634 });
1635 let result = ty.substitute_templates(&bindings_t_string());
1636 let Atomic::TConditional {
1637 param_name: _,
1638 subject,
1639 if_true,
1640 if_false,
1641 } = &result.types[0]
1642 else {
1643 panic!("expected TConditional");
1644 };
1645 assert!(matches!(subject.types[0], Atomic::TString));
1646 assert!(matches!(if_true.types[0], Atomic::TString));
1647 assert!(matches!(if_false.types[0], Atomic::TInt));
1648 }
1649
1650 #[test]
1651 fn resolve_conditional_is_null_non_null_arg() {
1652 let ty = Type::single(Atomic::TConditional {
1653 param_name: Some(Name::new("x")),
1654 subject: Box::new(Type::single(Atomic::TNull)),
1655 if_true: Box::new(Type::single(Atomic::TInt)),
1656 if_false: Box::new(Type::single(Atomic::TString)),
1657 });
1658 let result = ty.resolve_conditional_returns(|name| {
1659 if name == "x" {
1660 Some(Type::single(Atomic::TString)) } else {
1662 None
1663 }
1664 });
1665 assert!(result.types.len() == 1);
1666 assert!(matches!(result.types[0], Atomic::TString));
1667 }
1668
1669 #[test]
1670 fn resolve_conditional_is_null_null_arg() {
1671 let ty = Type::single(Atomic::TConditional {
1672 param_name: Some(Name::new("x")),
1673 subject: Box::new(Type::single(Atomic::TNull)),
1674 if_true: Box::new(Type::single(Atomic::TInt)),
1675 if_false: Box::new(Type::single(Atomic::TString)),
1676 });
1677 let result = ty.resolve_conditional_returns(|name| {
1678 if name == "x" {
1679 Some(Type::single(Atomic::TNull)) } else {
1681 None
1682 }
1683 });
1684 assert!(result.types.len() == 1);
1685 assert!(matches!(result.types[0], Atomic::TInt));
1686 }
1687
1688 #[test]
1689 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
1690 let mut nullable_str = Type::single(Atomic::TString);
1691 nullable_str.add_type(Atomic::TNull);
1692 let ty = Type::single(Atomic::TConditional {
1693 param_name: Some(Name::new("x")),
1694 subject: Box::new(Type::single(Atomic::TNull)),
1695 if_true: Box::new(Type::single(Atomic::TInt)),
1696 if_false: Box::new(Type::single(Atomic::TString)),
1697 });
1698 let result = ty.resolve_conditional_returns(|name| {
1699 if name == "x" {
1700 Some(nullable_str.clone())
1701 } else {
1702 None
1703 }
1704 });
1705 assert_eq!(result.types.len(), 2);
1707 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1708 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1709 }
1710
1711 #[test]
1712 fn resolve_conditional_nested_widens_inner_branch() {
1713 let inner = Type::single(Atomic::TConditional {
1716 param_name: Some(Name::new("x")),
1717 subject: Box::new(Type::single(Atomic::TString)),
1718 if_true: Box::new(Type::single(Atomic::TString)),
1719 if_false: Box::new(Type::single(Atomic::TFloat)),
1720 });
1721 let ty = Type::single(Atomic::TConditional {
1722 param_name: Some(Name::new("x")),
1723 subject: Box::new(Type::single(Atomic::TNull)),
1724 if_true: Box::new(Type::single(Atomic::TInt)),
1725 if_false: Box::new(inner),
1726 });
1727 let result = ty.resolve_conditional_returns(|_| None);
1729 assert!(
1730 result
1731 .types
1732 .iter()
1733 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1734 "no TConditional should survive: {:?}",
1735 result.types
1736 );
1737 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1738 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1739 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
1740 }
1741
1742 #[test]
1743 fn resolve_conditional_nested_resolves_inner_branch() {
1744 let inner = Type::single(Atomic::TConditional {
1748 param_name: Some(Name::new("x")),
1749 subject: Box::new(Type::single(Atomic::TString)),
1750 if_true: Box::new(Type::single(Atomic::TString)),
1751 if_false: Box::new(Type::single(Atomic::TFloat)),
1752 });
1753 let ty = Type::single(Atomic::TConditional {
1754 param_name: Some(Name::new("x")),
1755 subject: Box::new(Type::single(Atomic::TNull)),
1756 if_true: Box::new(Type::single(Atomic::TInt)),
1757 if_false: Box::new(inner),
1758 });
1759 let result = ty.resolve_conditional_returns(|name| {
1761 if name == "x" {
1762 Some(Type::single(Atomic::TString))
1763 } else {
1764 None
1765 }
1766 });
1767 assert!(
1768 result
1769 .types
1770 .iter()
1771 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1772 "no TConditional should survive: {:?}",
1773 result.types
1774 );
1775 assert_eq!(result.types.len(), 1);
1776 assert!(matches!(result.types[0], Atomic::TString));
1777 }
1778
1779 #[test]
1780 fn substitute_intersection_parts() {
1781 let ty = Type::single(Atomic::TIntersection {
1782 parts: vec_to_type_params(vec![
1783 Type::single(Atomic::TNamedObject {
1784 fqcn: Name::new("Countable"),
1785 type_params: empty_type_params(),
1786 }),
1787 t_param("T"),
1788 ]),
1789 });
1790 let result = ty.substitute_templates(&bindings_t_string());
1791 let Atomic::TIntersection { parts } = &result.types[0] else {
1792 panic!("expected TIntersection");
1793 };
1794 assert_eq!(parts.len(), 2);
1795 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1796 assert!(matches!(parts[1].types[0], Atomic::TString));
1797 }
1798
1799 #[test]
1800 fn substitute_no_template_params_identity() {
1801 let ty = Type::single(Atomic::TInt);
1802 let result = ty.substitute_templates(&bindings_t_string());
1803 assert!(matches!(result.types[0], Atomic::TInt));
1804 }
1805}