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 array_key() -> Self {
112 let mut u = Self::single(Atomic::TInt);
113 u.add_type(Atomic::TString);
114 u
115 }
116
117 pub fn nullable(atomic: Atomic) -> Self {
119 if matches!(atomic, Atomic::TMixed) {
121 return Self::mixed();
122 }
123 let mut types = SmallVec::new();
124 types.push(atomic);
125 types.push(Atomic::TNull);
126 Self {
127 types,
128 possibly_undefined: false,
129 from_docblock: false,
130 }
131 }
132
133 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
135 let mut u = Self::empty();
136 for a in atomics {
137 u.add_type(a);
138 }
139 u
140 }
141
142 pub fn is_empty(&self) -> bool {
145 self.types.is_empty()
146 }
147
148 pub fn is_single(&self) -> bool {
149 self.types.len() == 1
150 }
151
152 pub fn is_nullable(&self) -> bool {
153 self.types.iter().any(|t| matches!(t, Atomic::TNull))
154 }
155
156 pub fn is_array_key(&self) -> bool {
161 self.types.len() == 2
162 && self.types.iter().any(|t| matches!(t, Atomic::TInt))
163 && self.types.iter().any(|t| matches!(t, Atomic::TString))
164 }
165
166 pub fn is_mixed(&self) -> bool {
167 self.types.iter().any(|t| match t {
168 Atomic::TMixed => true,
169 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
170 _ => false,
171 })
172 }
173
174 pub fn is_mixed_not_template(&self) -> bool {
179 self.is_mixed()
180 && !self
181 .types
182 .iter()
183 .any(|t| matches!(t, Atomic::TTemplateParam { .. }))
184 }
185
186 pub fn is_never(&self) -> bool {
187 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
188 }
189
190 pub fn clone_validity(&self) -> CloneValidity {
193 if self.types.is_empty() {
194 return CloneValidity::Unknown;
195 }
196 let mut has_non_object = false;
197 let mut has_other = false; for t in &self.types {
199 match t {
200 Atomic::TTemplateParam { as_type, .. } => match as_type.clone_validity() {
201 CloneValidity::Invalid => has_non_object = true,
202 CloneValidity::PossiblyInvalid => {
203 has_non_object = true;
204 has_other = true;
205 }
206 CloneValidity::Cloneable | CloneValidity::Unknown => has_other = true,
207 },
208 other if other.is_definitely_non_object() => has_non_object = true,
209 _ => has_other = true,
210 }
211 }
212 match (has_non_object, has_other) {
213 (true, false) => CloneValidity::Invalid,
214 (true, true) => CloneValidity::PossiblyInvalid,
215 _ => CloneValidity::Cloneable,
216 }
217 }
218
219 pub fn is_void(&self) -> bool {
220 self.is_single() && matches!(self.types[0], Atomic::TVoid)
221 }
222
223 pub fn can_be_falsy(&self) -> bool {
224 self.types.iter().any(|t| t.can_be_falsy())
225 }
226
227 pub fn can_be_truthy(&self) -> bool {
228 self.types.iter().any(|t| t.can_be_truthy())
229 }
230
231 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
232 self.types.iter().any(f)
233 }
234
235 pub fn has_named_object(&self, fqcn: &str) -> bool {
236 self.types.iter().any(|t| match t {
237 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
238 _ => false,
239 })
240 }
241
242 pub fn add_type(&mut self, atomic: Atomic) {
247 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
249 return;
250 }
251
252 if matches!(atomic, Atomic::TMixed) {
254 self.types.clear();
255 self.types.push(Atomic::TMixed);
256 return;
257 }
258
259 let atomic = if let Atomic::TConditional { data } = &atomic {
262 let (if_true, if_false) = (&data.if_true, &data.if_false);
263 let mut simplified_true = Type::empty();
264 for t in &if_true.types {
265 simplified_true.add_type(t.clone());
266 }
267 let mut simplified_false = Type::empty();
268 for t in &if_false.types {
269 simplified_false.add_type(t.clone());
270 }
271 if simplified_true == simplified_false {
272 for t in simplified_true.types {
273 self.add_type(t);
274 }
275 return;
276 }
277 atomic
278 } else {
279 atomic
280 };
281
282 if self.types.contains(&atomic) {
284 return;
285 }
286
287 if let Atomic::TLiteralInt(_) = &atomic {
289 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
290 return;
291 }
292 }
293 if let Atomic::TLiteralString(_) = &atomic {
295 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
296 return;
297 }
298 }
299 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
301 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
302 {
303 return;
304 }
305 if matches!(atomic, Atomic::TTrue) && self.types.iter().any(|t| matches!(t, Atomic::TFalse))
308 {
309 self.types.retain(|t| !matches!(t, Atomic::TFalse));
310 self.types.push(Atomic::TBool);
311 return;
312 }
313 if matches!(atomic, Atomic::TFalse) && self.types.iter().any(|t| matches!(t, Atomic::TTrue))
314 {
315 self.types.retain(|t| !matches!(t, Atomic::TTrue));
316 self.types.push(Atomic::TBool);
317 return;
318 }
319 if matches!(atomic, Atomic::TInt) {
321 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
322 }
323 if matches!(atomic, Atomic::TString) {
325 self.types
326 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
327 }
328 if matches!(atomic, Atomic::TBool) {
330 self.types
331 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
332 }
333
334 if matches!(atomic, Atomic::TNever) {
336 if !self.types.is_empty() {
337 return;
338 }
339 } else {
340 self.types.retain(|t| !matches!(t, Atomic::TNever));
341 }
342
343 if let Atomic::TKeyedArray {
349 properties,
350 is_open,
351 ..
352 } = &atomic
353 {
354 if properties.is_empty() && !is_open {
355 for existing in &self.types {
356 match existing {
357 Atomic::TArray { .. }
358 | Atomic::TNonEmptyArray { .. }
359 | Atomic::TList { .. }
360 | Atomic::TNonEmptyList { .. } => {
361 return; }
363 _ => {}
364 }
365 }
366 }
367 }
368
369 let is_generic_array_or_list = matches!(
372 &atomic,
373 Atomic::TArray { .. }
374 | Atomic::TNonEmptyArray { .. }
375 | Atomic::TList { .. }
376 | Atomic::TNonEmptyList { .. }
377 );
378 if is_generic_array_or_list {
379 self.types.retain(|t| {
380 if let Atomic::TKeyedArray {
381 properties,
382 is_open,
383 ..
384 } = t
385 {
386 !properties.is_empty() || *is_open
387 } else {
388 true
389 }
390 });
391 }
392
393 self.types.push(atomic);
394 }
395
396 pub fn remove_null(&self) -> Type {
400 self.filter(|t| !matches!(t, Atomic::TNull))
401 }
402
403 pub fn remove_false(&self) -> Type {
406 let mut result = self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool));
407 if self.types.iter().any(|t| matches!(t, Atomic::TBool)) {
408 result.add_type(Atomic::TTrue);
409 }
410 result
411 }
412
413 pub fn core_type(&self) -> Type {
415 self.remove_null().remove_false()
416 }
417
418 pub fn narrow_to_truthy(&self) -> Type {
420 if self.is_mixed_not_template() {
421 return Type::mixed();
422 }
423 let mut result = Type::empty();
424 result.from_docblock = self.from_docblock;
425 for t in &self.types {
426 match t {
427 Atomic::TTemplateParam { .. } => result.add_type(t.clone()),
431 Atomic::TLiteralInt(0)
433 | Atomic::TLiteralFloat(0, 0)
434 | Atomic::TNull
435 | Atomic::TFalse => {}
436 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => {}
437 Atomic::TBool => result.add_type(Atomic::TTrue),
439 Atomic::TArray { key, value } => result.add_type(Atomic::TNonEmptyArray {
441 key: key.clone(),
442 value: value.clone(),
443 }),
444 Atomic::TList { value } => result.add_type(Atomic::TNonEmptyList {
445 value: value.clone(),
446 }),
447 Atomic::TString => result.add_type(Atomic::TNonEmptyString),
451 Atomic::TNonNegativeInt => result.add_type(Atomic::TPositiveInt),
456 Atomic::TIntRange { min: Some(0), max } if max.is_none_or(|m| m >= 1) => {
457 let atom = if max.is_none() {
458 Atomic::TPositiveInt
459 } else {
460 Atomic::TIntRange {
461 min: Some(1),
462 max: *max,
463 }
464 };
465 result.add_type(atom);
466 }
467 Atomic::TIntRange { min, max: Some(0) } => {
469 let atom = match min {
470 None => Atomic::TNegativeInt,
471 Some(n) if *n <= -1 => Atomic::TIntRange {
472 min: *min,
473 max: Some(-1),
474 },
475 _ => continue, };
477 result.add_type(atom);
478 }
479 t if !t.can_be_truthy() => {}
481 _ => result.add_type(t.clone()),
482 }
483 }
484 result
485 }
486
487 pub fn narrow_to_falsy(&self) -> Type {
489 if self.is_mixed_not_template() {
490 return Type::from_vec(vec![
491 Atomic::TNull,
492 Atomic::TFalse,
493 Atomic::TLiteralInt(0),
494 Atomic::TLiteralString("".into()),
495 ]);
496 }
497 let mut result = Type::empty();
498 result.from_docblock = self.from_docblock;
499 for t in &self.types {
500 match t {
501 Atomic::TTemplateParam { .. } => result.add_type(t.clone()),
506 Atomic::TBool => result.add_type(Atomic::TFalse),
508 Atomic::TInt => result.add_type(Atomic::TLiteralInt(0)),
510 Atomic::TFloat => result.add_type(Atomic::TLiteralFloat(0, 0)),
512 Atomic::TString => {
514 result.add_type(Atomic::TLiteralString("".into()));
515 result.add_type(Atomic::TLiteralString("0".into()));
516 }
517 Atomic::TNumericString => result.add_type(Atomic::TLiteralString("0".into())),
519 Atomic::TNonNegativeInt => result.add_type(Atomic::TLiteralInt(0)),
521 Atomic::TIntRange {
523 min: Some(0),
524 max: Some(_) | None,
525 } => result.add_type(Atomic::TLiteralInt(0)),
526 Atomic::TIntRange { max: Some(0), .. } => result.add_type(Atomic::TLiteralInt(0)),
528 t if !t.can_be_falsy() => {} _ => result.add_type(t.clone()),
530 }
531 }
532 result
533 }
534
535 pub fn narrow_instanceof(&self, class: &str) -> Type {
541 let narrowed_ty = Atomic::TNamedObject {
542 fqcn: class.into(),
543 type_params: empty_type_params(),
544 };
545 let has_object = self.types.iter().any(|t| {
547 matches!(
548 t,
549 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
551 });
552 if has_object || self.is_empty() {
553 Type::single(narrowed_ty)
554 } else {
555 Type::single(narrowed_ty)
558 }
559 }
560
561 pub fn narrow_to_string(&self) -> Type {
565 self.filter_replacing(
566 |t| t.is_string() || matches!(t, Atomic::TTemplateParam { .. }),
567 |t| matches!(t, Atomic::TMixed | Atomic::TScalar),
568 Atomic::TString,
569 )
570 }
571
572 pub fn narrow_to_int(&self) -> Type {
574 self.filter_replacing(
575 |t| t.is_int() || matches!(t, Atomic::TTemplateParam { .. }),
576 |t| matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric),
577 Atomic::TInt,
578 )
579 }
580
581 pub fn narrow_to_float(&self) -> Type {
583 self.filter_replacing(
584 |t| {
585 matches!(
586 t,
587 Atomic::TFloat
588 | Atomic::TIntegralFloat
589 | Atomic::TLiteralFloat(..)
590 | Atomic::TTemplateParam { .. }
591 )
592 },
593 |t| matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric),
594 Atomic::TFloat,
595 )
596 }
597
598 pub fn narrow_to_bool(&self) -> Type {
600 self.filter_replacing(
601 |t| {
602 matches!(
603 t,
604 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TTemplateParam { .. }
605 )
606 },
607 |t| matches!(t, Atomic::TMixed | Atomic::TScalar),
608 Atomic::TBool,
609 )
610 }
611
612 pub fn narrow_to_null(&self) -> Type {
614 self.filter_replacing(
615 |t| matches!(t, Atomic::TNull | Atomic::TTemplateParam { .. }),
616 |t| matches!(t, Atomic::TMixed),
617 Atomic::TNull,
618 )
619 }
620
621 pub fn narrow_to_array(&self) -> Type {
623 self.filter_replacing(
624 |t| t.is_array() || matches!(t, Atomic::TTemplateParam { .. }),
625 |t| matches!(t, Atomic::TMixed),
626 Atomic::TArray {
627 key: Box::new(Type::mixed()),
628 value: Box::new(Type::mixed()),
629 },
630 )
631 }
632
633 pub fn narrow_to_non_empty_collection(&self) -> Type {
635 let mut out = Type::empty();
636 out.from_docblock = self.from_docblock;
637 for t in &self.types {
638 match t {
639 Atomic::TArray { key, value } => out.add_type(Atomic::TNonEmptyArray {
640 key: key.clone(),
641 value: value.clone(),
642 }),
643 Atomic::TList { value } => out.add_type(Atomic::TNonEmptyList {
644 value: value.clone(),
645 }),
646 _ => out.add_type(t.clone()),
647 }
648 }
649 out
650 }
651
652 pub fn narrow_to_empty_collection(&self) -> Type {
662 let mut out = Type::empty();
663 out.from_docblock = self.from_docblock;
664 for t in &self.types {
665 match t {
666 Atomic::TNonEmptyArray { .. } | Atomic::TNonEmptyList { .. } => {}
667 Atomic::TArray { .. } | Atomic::TList { .. } => {
668 out.add_type(Atomic::TKeyedArray {
669 properties: Box::default(),
670 is_open: false,
671 is_list: true,
672 });
673 }
674 _ => out.add_type(t.clone()),
675 }
676 }
677 out
678 }
679
680 pub fn narrow_to_list(&self) -> Type {
692 let mut out = Type::empty();
693 out.from_docblock = self.from_docblock;
694 for t in &self.types {
695 match t {
696 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => out.add_type(t.clone()),
697 Atomic::TArray { key, value } if matches!(key.types.as_slice(), [Atomic::TInt]) => {
698 out.add_type(Atomic::TList {
699 value: value.clone(),
700 });
701 }
702 Atomic::TNonEmptyArray { key, value }
703 if matches!(key.types.as_slice(), [Atomic::TInt]) =>
704 {
705 out.add_type(Atomic::TNonEmptyList {
706 value: value.clone(),
707 });
708 }
709 Atomic::TKeyedArray { is_list: true, .. } => out.add_type(t.clone()),
710 Atomic::TMixed => out.add_type(Atomic::TList {
711 value: Box::new(Type::mixed()),
712 }),
713 _ => {}
714 }
715 }
716 if out.is_empty() {
717 self.filter(|t| matches!(t, Atomic::TList { .. } | Atomic::TNonEmptyList { .. }))
718 } else {
719 out
720 }
721 }
722
723 pub fn narrow_to_object(&self) -> Type {
728 let mut out = Type::empty();
729 for t in &self.types {
730 if matches!(t, Atomic::TMixed) {
731 out.add_type(Atomic::TObject);
732 } else if t.is_object() || matches!(t, Atomic::TTemplateParam { .. }) {
733 out.add_type(t.clone());
734 }
735 }
736 if out.types.is_empty() {
737 self.filter(|t| t.is_object())
738 } else {
739 out
740 }
741 }
742
743 pub fn narrow_to_callable(&self) -> Type {
750 self.filter(|t| {
751 t.is_callable()
752 || t.is_string()
753 || t.is_array()
754 || t.is_object()
755 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
756 })
757 }
758
759 pub fn narrow_to_scalar(&self) -> Type {
761 self.filter_replacing(
762 |t| {
763 t.is_string()
764 || t.is_int()
765 || matches!(
766 t,
767 Atomic::TFloat
768 | Atomic::TIntegralFloat
769 | Atomic::TLiteralFloat(..)
770 | Atomic::TBool
771 | Atomic::TTrue
772 | Atomic::TFalse
773 | Atomic::TScalar
774 | Atomic::TNumeric
775 | Atomic::TNumericString
776 | Atomic::TTemplateParam { .. }
777 )
778 },
779 |t| matches!(t, Atomic::TMixed),
780 Atomic::TScalar,
781 )
782 }
783
784 pub fn narrow_to_iterable(&self) -> Type {
787 self.filter(|t| {
788 t.is_array()
789 || t.is_object()
790 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
791 })
792 }
793
794 pub fn narrow_to_countable(&self) -> Type {
797 self.filter(|t| {
798 t.is_array()
799 || t.is_object()
800 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
801 })
802 }
803
804 pub fn narrow_to_resource(&self) -> Type {
808 self.filter(|t| matches!(t, Atomic::TMixed))
810 }
811
812 pub fn narrow_to_class_string(&self) -> Type {
817 let mut out = Type::empty();
818 out.from_docblock = self.from_docblock;
819 for t in &self.types {
820 match t {
821 Atomic::TClassString(_) => out.add_type(t.clone()),
822 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
823 out.add_type(Atomic::TClassString(None));
824 }
825 _ => {}
826 }
827 }
828 out
829 }
830
831 pub fn narrow_to_interface_string(&self) -> Type {
836 let mut out = Type::empty();
837 out.from_docblock = self.from_docblock;
838 for t in &self.types {
839 match t {
840 Atomic::TInterfaceString(_) => out.add_type(t.clone()),
841 Atomic::TClassString(name) => {
845 out.add_type(Atomic::TInterfaceString(*name));
846 }
847 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
848 out.add_type(Atomic::TInterfaceString(None));
849 }
850 _ => {}
851 }
852 }
853 out
854 }
855
856 pub fn merge(a: &Type, b: &Type) -> Type {
861 if b.types.is_empty() {
863 let mut result = a.clone();
864 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
865 return result;
866 }
867 if a.types.is_empty() {
869 let mut result = b.clone();
870 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
871 return result;
872 }
873 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
875 let mut result = a.clone();
876 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
877 return result;
878 }
879 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
881 return Type {
882 types: smallvec::smallvec![Atomic::TMixed],
883 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
884 from_docblock: a.from_docblock || b.from_docblock,
885 };
886 }
887 let mut result = a.clone();
888 result.merge_with(b);
889 result
890 }
891
892 pub fn merge_with(&mut self, other: &Type) {
894 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
895 self.possibly_undefined |= other.possibly_undefined;
896 return;
897 }
898 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
899 self.types.clear();
900 self.types.push(Atomic::TMixed);
901 self.possibly_undefined |= other.possibly_undefined;
902 return;
903 }
904 for atomic in &other.types {
905 self.add_type(atomic.clone());
906 }
907 self.possibly_undefined |= other.possibly_undefined;
908 }
909
910 pub fn intersect_with(&self, other: &Type) -> Type {
914 if self.is_mixed() {
915 return other.clone();
916 }
917 if other.is_mixed() {
918 return self.clone();
919 }
920 let mut result = Type::empty();
928 for a in &self.types {
929 for b in &other.types {
930 if a == b {
931 result.add_type(a.clone());
932 } else if atomic_subtype(b, a) {
933 result.add_type(b.clone());
934 } else if atomic_subtype(a, b) {
935 result.add_type(a.clone());
936 }
937 }
938 }
939 if result.is_empty() {
940 Type::never()
941 } else {
942 result
943 }
944 }
945
946 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
950 if bindings.is_empty() {
951 return self.clone();
952 }
953 if !self.types.iter().any(atomic_may_contain_templates) {
956 return self.clone();
957 }
958 let mut result = Type::empty();
959 result.possibly_undefined = self.possibly_undefined;
960 result.from_docblock = self.from_docblock;
961 for atomic in &self.types {
962 match atomic {
963 Atomic::TTemplateParam { name, .. } => {
964 if let Some(resolved) = bindings.get(name) {
965 for t in &resolved.types {
966 result.add_type(t.clone());
967 }
968 } else {
969 result.add_type(atomic.clone());
970 }
971 }
972 Atomic::TArray { key, value } => {
973 result.add_type(Atomic::TArray {
974 key: Box::new(key.substitute_templates(bindings)),
975 value: Box::new(value.substitute_templates(bindings)),
976 });
977 }
978 Atomic::TList { value } => {
979 result.add_type(Atomic::TList {
980 value: Box::new(value.substitute_templates(bindings)),
981 });
982 }
983 Atomic::TNonEmptyArray { key, value } => {
984 result.add_type(Atomic::TNonEmptyArray {
985 key: Box::new(key.substitute_templates(bindings)),
986 value: Box::new(value.substitute_templates(bindings)),
987 });
988 }
989 Atomic::TNonEmptyList { value } => {
990 result.add_type(Atomic::TNonEmptyList {
991 value: Box::new(value.substitute_templates(bindings)),
992 });
993 }
994 Atomic::TKeyedArray {
995 properties,
996 is_open,
997 is_list,
998 } => {
999 use crate::atomic::KeyedProperty;
1000 let new_props = properties
1001 .iter()
1002 .map(|(k, prop)| {
1003 (
1004 k.clone(),
1005 KeyedProperty {
1006 ty: prop.ty.substitute_templates(bindings),
1007 optional: prop.optional,
1008 },
1009 )
1010 })
1011 .collect();
1012 result.add_type(Atomic::TKeyedArray {
1013 properties: Box::new(new_props),
1014 is_open: *is_open,
1015 is_list: *is_list,
1016 });
1017 }
1018 Atomic::TCallable {
1019 params,
1020 return_type,
1021 } => {
1022 result.add_type(Atomic::TCallable {
1023 params: params.as_ref().map(|ps| {
1024 ps.iter()
1025 .map(|p| substitute_in_fn_param(p, bindings))
1026 .collect()
1027 }),
1028 return_type: return_type
1029 .as_ref()
1030 .map(|r| Box::new(r.substitute_templates(bindings))),
1031 });
1032 }
1033 Atomic::TClosure { data } => {
1034 result.add_type(Atomic::TClosure {
1035 data: Box::new(crate::atomic::ClosureData {
1036 params: data
1037 .params
1038 .iter()
1039 .map(|p| substitute_in_fn_param(p, bindings))
1040 .collect(),
1041 return_type: data.return_type.substitute_templates(bindings),
1042 this_type: data
1043 .this_type
1044 .as_ref()
1045 .map(|t| t.substitute_templates(bindings)),
1046 }),
1047 });
1048 }
1049 Atomic::TConditional { data } => {
1050 let param_name = &data.param_name;
1051 let new_subject = data.subject.substitute_templates(bindings);
1052 let new_if_true = data.if_true.substitute_templates(bindings);
1053 let new_if_false = data.if_false.substitute_templates(bindings);
1054
1055 let resolved = if let Some(name) = param_name {
1059 if let Some(bound) = bindings.get(name) {
1060 if new_subject.types.len() == 1 {
1061 resolve_conditional_branch(
1062 &new_subject.types[0],
1063 bound,
1064 &new_if_true,
1065 &new_if_false,
1066 )
1067 } else {
1068 None
1069 }
1070 } else {
1071 None
1072 }
1073 } else {
1074 None
1075 };
1076
1077 if let Some(branch) = resolved {
1078 for t in branch.types {
1079 result.add_type(t);
1080 }
1081 } else {
1082 result.add_type(Atomic::TConditional {
1083 data: Box::new(crate::atomic::ConditionalData {
1084 param_name: *param_name,
1085 subject: new_subject,
1086 if_true: new_if_true,
1087 if_false: new_if_false,
1088 }),
1089 });
1090 }
1091 }
1092 Atomic::TIntersection { parts } => {
1093 result.add_type(Atomic::TIntersection {
1094 parts: vec_to_type_params(
1095 parts
1096 .iter()
1097 .map(|p| p.substitute_templates(bindings))
1098 .collect(),
1099 ),
1100 });
1101 }
1102 Atomic::TNamedObject { fqcn, type_params } => {
1103 if type_params.is_empty() && !fqcn.contains('\\') {
1110 if let Some(resolved) = bindings.get(fqcn) {
1111 for t in &resolved.types {
1112 result.add_type(t.clone());
1113 }
1114 continue;
1115 }
1116 }
1117 let new_params: Vec<Type> = type_params
1118 .iter()
1119 .map(|p| p.substitute_templates(bindings))
1120 .collect();
1121 result.add_type(Atomic::TNamedObject {
1122 fqcn: *fqcn,
1123 type_params: vec_to_type_params(new_params),
1124 });
1125 }
1126 Atomic::TClassString(Some(param_name)) => {
1128 if let Some(resolved) = bindings.get(param_name) {
1129 for r_atomic in &resolved.types {
1130 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
1131 Some(*fqcn)
1132 } else {
1133 None
1134 };
1135 result.add_type(Atomic::TClassString(cls_name));
1136 }
1137 } else {
1138 result.add_type(atomic.clone());
1139 }
1140 }
1141 Atomic::TInterfaceString(Some(param_name)) => {
1143 if let Some(resolved) = bindings.get(param_name) {
1144 for r_atomic in &resolved.types {
1145 let iface_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
1146 Some(*fqcn)
1147 } else {
1148 None
1149 };
1150 result.add_type(Atomic::TInterfaceString(iface_name));
1151 }
1152 } else {
1153 result.add_type(atomic.clone());
1154 }
1155 }
1156 _ => {
1157 result.add_type(atomic.clone());
1158 }
1159 }
1160 }
1161 result
1162 }
1163
1164 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
1170 where
1171 F: Fn(&str) -> Option<Type>,
1172 {
1173 self.resolve_conditional_inner(&lookup)
1174 }
1175
1176 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
1177 where
1178 F: Fn(&str) -> Option<Type>,
1179 {
1180 let mut result = Type::empty();
1181 for atomic in self.types {
1182 match atomic {
1183 Atomic::TConditional { ref data } => {
1184 let (param_name, subject, if_true, if_false) = (
1185 &data.param_name,
1186 &data.subject,
1187 &data.if_true,
1188 &data.if_false,
1189 );
1190 let resolved = if subject.types.len() == 1 {
1191 if let Some(name) = param_name {
1192 if let Some(arg_ty) = lookup(name.as_ref()) {
1193 resolve_conditional_branch(
1194 &subject.types[0],
1195 &arg_ty,
1196 if_true,
1197 if_false,
1198 )
1199 } else {
1200 None
1201 }
1202 } else {
1203 None
1204 }
1205 } else {
1206 None
1207 };
1208
1209 if let Some(branch) = resolved {
1210 for t in branch.resolve_conditional_inner(lookup).types {
1212 result.add_type(t);
1213 }
1214 } else {
1215 for t in if_true.clone().resolve_conditional_inner(lookup).types {
1218 result.add_type(t);
1219 }
1220 for t in if_false.clone().resolve_conditional_inner(lookup).types {
1221 result.add_type(t);
1222 }
1223 }
1224 }
1225 other => result.add_type(other),
1226 }
1227 }
1228 result
1229 }
1230
1231 pub fn is_subtype_structural(&self, other: &Type) -> bool {
1241 if other.is_mixed() {
1242 return true;
1243 }
1244 if self.is_never() {
1245 return true; }
1247 self.types
1248 .iter()
1249 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
1250 }
1251
1252 pub fn accepts_atomic_structural(&self, sub: &Atomic) -> bool {
1256 if self.is_mixed() {
1257 return true;
1258 }
1259 matches!(sub, Atomic::TNever) || self.types.iter().any(|b| atomic_subtype(sub, b))
1260 }
1261
1262 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
1265 let mut result = Type::empty();
1266 result.possibly_undefined = self.possibly_undefined;
1267 result.from_docblock = self.from_docblock;
1268 for atomic in &self.types {
1269 if f(atomic) {
1270 result.types.push(atomic.clone());
1271 }
1272 }
1273 result
1274 }
1275
1276 fn filter_replacing<K: Fn(&Atomic) -> bool, P: Fn(&Atomic) -> bool>(
1281 &self,
1282 keep: K,
1283 placeholder: P,
1284 replacement: Atomic,
1285 ) -> Type {
1286 let mut result = Type::empty();
1287 result.possibly_undefined = self.possibly_undefined;
1288 result.from_docblock = self.from_docblock;
1289 for atomic in &self.types {
1290 if keep(atomic) {
1291 result.add_type(atomic.clone());
1292 } else if placeholder(atomic) {
1293 result.add_type(replacement.clone());
1294 }
1295 }
1296 result
1297 }
1298
1299 pub fn possibly_undefined(mut self) -> Self {
1301 self.possibly_undefined = true;
1302 self
1303 }
1304
1305 pub fn from_docblock(mut self) -> Self {
1307 self.from_docblock = true;
1308 self
1309 }
1310}
1311
1312fn is_string_atomic(a: &Atomic) -> bool {
1317 matches!(
1318 a,
1319 Atomic::TString
1320 | Atomic::TNonEmptyString
1321 | Atomic::TLiteralString(_)
1322 | Atomic::TNumericString
1323 | Atomic::TClassString(_)
1324 | Atomic::TInterfaceString(_)
1325 | Atomic::TCallableString
1326 )
1327}
1328
1329fn is_array_atomic(a: &Atomic) -> bool {
1330 matches!(
1331 a,
1332 Atomic::TArray { .. }
1333 | Atomic::TNonEmptyArray { .. }
1334 | Atomic::TKeyedArray { .. }
1335 | Atomic::TList { .. }
1336 | Atomic::TNonEmptyList { .. }
1337 )
1338}
1339
1340fn is_list_atomic(a: &Atomic) -> bool {
1341 match a {
1342 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
1343 Atomic::TKeyedArray { is_list, .. } => *is_list,
1344 _ => false,
1345 }
1346}
1347
1348fn is_float_atomic(a: &Atomic) -> bool {
1349 matches!(
1350 a,
1351 Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
1352 )
1353}
1354
1355fn is_bool_atomic(a: &Atomic) -> bool {
1356 matches!(a, Atomic::TBool | Atomic::TTrue | Atomic::TFalse)
1357}
1358
1359fn resolve_conditional_branch(
1365 subject: &Atomic,
1366 arg_ty: &Type,
1367 if_true: &Type,
1368 if_false: &Type,
1369) -> Option<Type> {
1370 let predicate: fn(&Atomic) -> bool = match subject {
1371 Atomic::TNull => |a| matches!(a, Atomic::TNull),
1372 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
1373 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
1374 Atomic::TString => is_string_atomic,
1375 Atomic::TList { .. } => is_list_atomic,
1376 Atomic::TArray { .. } => is_array_atomic,
1377 Atomic::TInt => Atomic::is_int,
1378 Atomic::TFloat => is_float_atomic,
1379 Atomic::TBool => is_bool_atomic,
1380 _ => return None,
1381 };
1382
1383 if arg_ty.types.is_empty() {
1384 return None;
1385 }
1386 let all_match = arg_ty.types.iter().all(&predicate);
1387 let none_match = !arg_ty.types.iter().any(predicate);
1388 if all_match {
1389 Some(if_true.clone())
1390 } else if none_match {
1391 Some(if_false.clone())
1392 } else {
1393 None
1394 }
1395}
1396
1397fn atomic_may_contain_templates(atomic: &Atomic) -> bool {
1405 match atomic {
1406 Atomic::TNamedObject { fqcn, type_params } => {
1410 !type_params.is_empty() || !fqcn.contains('\\')
1411 }
1412 Atomic::TTemplateParam { .. }
1413 | Atomic::TArray { .. }
1414 | Atomic::TList { .. }
1415 | Atomic::TNonEmptyArray { .. }
1416 | Atomic::TNonEmptyList { .. }
1417 | Atomic::TKeyedArray { .. }
1418 | Atomic::TCallable { .. }
1419 | Atomic::TClosure { .. }
1420 | Atomic::TConditional { .. }
1421 | Atomic::TIntersection { .. }
1422 | Atomic::TClassString(Some(_))
1423 | Atomic::TInterfaceString(Some(_)) => true,
1424 _ => false,
1425 }
1426}
1427
1428fn substitute_in_fn_param(
1429 p: &crate::atomic::FnParam,
1430 bindings: &FxHashMap<Name, Type>,
1431) -> crate::atomic::FnParam {
1432 crate::atomic::FnParam {
1433 name: p.name,
1434 ty: p.ty.as_ref().map(|t| {
1435 let u = t.to_union();
1436 let substituted = u.substitute_templates(bindings);
1437 crate::compact::SimpleType::from_union(substituted)
1438 }),
1439 out_ty: p.out_ty.as_ref().map(|t| {
1440 let u = t.to_union();
1441 let substituted = u.substitute_templates(bindings);
1442 crate::compact::SimpleType::from_union(substituted)
1443 }),
1444 default: p.default.as_ref().map(|d| {
1445 let u = d.to_union();
1446 let substituted = u.substitute_templates(bindings);
1447 crate::compact::SimpleType::from_union(substituted)
1448 }),
1449 is_variadic: p.is_variadic,
1450 is_byref: p.is_byref,
1451 is_optional: p.is_optional,
1452 }
1453}
1454
1455pub fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1461 if sub == sup {
1462 return true;
1463 }
1464 match (sub, sup) {
1465 (Atomic::TNever, _) => true,
1467 (_, Atomic::TMixed) => true,
1469 (Atomic::TMixed, _) => true,
1470 (_, Atomic::TTemplateParam { as_type, .. }) => {
1475 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1476 }
1477
1478 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1480 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1481 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1482 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1483 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1484 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1485 (Atomic::TPositiveInt, Atomic::TInt) => true,
1486 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1487 (Atomic::TPositiveInt, Atomic::TNumeric) => true,
1488 (Atomic::TPositiveInt, Atomic::TScalar) => true,
1489 (Atomic::TNegativeInt, Atomic::TInt) => true,
1490 (Atomic::TNegativeInt, Atomic::TNumeric) => true,
1491 (Atomic::TNegativeInt, Atomic::TScalar) => true,
1492 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1493 (Atomic::TNonNegativeInt, Atomic::TNumeric) => true,
1494 (Atomic::TNonNegativeInt, Atomic::TScalar) => true,
1495 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1496 (Atomic::TIntRange { .. }, Atomic::TNumeric) => true,
1497 (Atomic::TIntRange { .. }, Atomic::TScalar) => true,
1498 (Atomic::TPositiveInt, Atomic::TIntRange { min, max }) => {
1500 max.is_none() && min.is_none_or(|m| m <= 1)
1501 }
1502 (Atomic::TNegativeInt, Atomic::TIntRange { min, max }) => {
1504 min.is_none() && max.is_none_or(|m| m >= -1)
1505 }
1506 (Atomic::TNonNegativeInt, Atomic::TIntRange { min, max }) => {
1508 max.is_none() && min.is_none_or(|m| m <= 0)
1509 }
1510 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TPositiveInt) => {
1512 sub_min.is_some_and(|lo| lo >= 1)
1513 }
1514 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TNonNegativeInt) => {
1515 sub_min.is_some_and(|lo| lo >= 0)
1516 }
1517 (Atomic::TIntRange { max: sub_max, .. }, Atomic::TNegativeInt) => {
1518 sub_max.is_some_and(|hi| hi <= -1)
1519 }
1520 (
1522 Atomic::TIntRange {
1523 min: sub_min,
1524 max: sub_max,
1525 },
1526 Atomic::TIntRange {
1527 min: sup_min,
1528 max: sup_max,
1529 },
1530 ) => {
1531 let lower_ok = match (sub_min, sup_min) {
1532 (_, None) => true,
1533 (None, Some(_)) => false,
1534 (Some(sl), Some(su)) => sl >= su,
1535 };
1536 let upper_ok = match (sub_max, sup_max) {
1537 (None, None) | (Some(_), None) => true,
1538 (None, Some(_)) => false,
1539 (Some(sl), Some(su)) => sl <= su,
1540 };
1541 lower_ok && upper_ok
1542 }
1543
1544 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1545 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1546 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1547
1548 (Atomic::TLiteralString(s), Atomic::TString) => {
1549 let _ = s;
1550 true
1551 }
1552 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1553 let _ = s;
1554 true
1555 }
1556 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1557 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1558 (Atomic::TLiteralString(_), Atomic::TClassString(_)) => true,
1561 (Atomic::TLiteralString(_), Atomic::TInterfaceString(_)) => true,
1564 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1565 (Atomic::TNonEmptyString, Atomic::TString) => true,
1566 (Atomic::TCallableString, Atomic::TString) => true,
1567 (Atomic::TNumericString, Atomic::TNonEmptyString) => true,
1569 (Atomic::TNumericString, Atomic::TString) => true,
1570 (Atomic::TClassString(_), Atomic::TString) => true,
1571 (Atomic::TInterfaceString(_), Atomic::TString) => true,
1572 (Atomic::TInterfaceString(_), Atomic::TClassString(None)) => true,
1577 (Atomic::TInterfaceString(Some(a)), Atomic::TClassString(Some(b))) => a == b,
1578 (Atomic::TEnumString, Atomic::TString) => true,
1579 (Atomic::TTraitString, Atomic::TString) => true,
1580
1581 (Atomic::TTrue, Atomic::TBool) => true,
1582 (Atomic::TFalse, Atomic::TBool) => true,
1583
1584 (Atomic::TInt, Atomic::TNumeric) => true,
1585 (Atomic::TFloat, Atomic::TNumeric) => true,
1586 (Atomic::TIntegralFloat, Atomic::TNumeric) => true,
1587 (Atomic::TNumericString, Atomic::TNumeric) => true,
1588
1589 (Atomic::TInt, Atomic::TScalar) => true,
1590 (Atomic::TFloat, Atomic::TScalar) => true,
1591 (Atomic::TIntegralFloat, Atomic::TScalar) => true,
1592 (Atomic::TString, Atomic::TScalar) => true,
1593 (Atomic::TBool, Atomic::TScalar) => true,
1594 (Atomic::TNumeric, Atomic::TScalar) => true,
1595 (Atomic::TTrue, Atomic::TScalar) => true,
1596 (Atomic::TFalse, Atomic::TScalar) => true,
1597
1598 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1600 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1601 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1602 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1604 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1605 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1607 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1608 (
1612 Atomic::TNamedObject {
1613 fqcn: sub_fqcn,
1614 type_params: sub_params,
1615 },
1616 Atomic::TNamedObject {
1617 fqcn: sup_fqcn,
1618 type_params: sup_params,
1619 },
1620 ) => {
1621 sub_fqcn == sup_fqcn
1622 && (sup_params.is_empty() || type_params_compatible(sub_params, sup_params))
1623 }
1624
1625 (Atomic::TIntegralFloat, Atomic::TFloat) => true,
1627
1628 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1630 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1631 (Atomic::TNegativeInt, Atomic::TFloat) => true,
1632 (Atomic::TNonNegativeInt, Atomic::TFloat) => true,
1633 (Atomic::TInt, Atomic::TFloat) => true,
1634 (Atomic::TIntRange { .. }, Atomic::TFloat) => true,
1635
1636 (Atomic::TLiteralInt(n), Atomic::TIntRange { min, max }) => {
1638 min.is_none_or(|lo| *n >= lo) && max.is_none_or(|hi| *n <= hi)
1639 }
1640
1641 (Atomic::TString, Atomic::TCallable { .. }) => true,
1643 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1644 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1645 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1646 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1647 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1648
1649 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1651 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1653 (Atomic::TClosure { data: sub }, Atomic::TClosure { data: sup }) => {
1663 fn has_nominal_type(t: &Type) -> bool {
1664 t.types.iter().any(|a| {
1665 matches!(
1666 a,
1667 Atomic::TNamedObject { .. }
1668 | Atomic::TSelf { .. }
1669 | Atomic::TStaticObject { .. }
1670 | Atomic::TTemplateParam { .. }
1671 | Atomic::TClosure { .. }
1672 | Atomic::TCallable { .. }
1673 )
1674 })
1675 }
1676 let sub_required = sub
1677 .params
1678 .iter()
1679 .filter(|p| !p.is_optional && !p.is_variadic)
1680 .count();
1681 if sub_required > sup.params.len() {
1682 false
1683 } else {
1684 let params_ok = sup.params.iter().enumerate().all(|(i, sup_param)| {
1685 let Some(sub_param) = sub.params.get(i) else {
1686 return true;
1687 };
1688 if sub_param.is_optional || sub_param.is_variadic {
1689 return true;
1690 }
1691 let (Some(sub_ty), Some(sup_ty)) =
1692 (sub_param.ty.as_ref(), sup_param.ty.as_ref())
1693 else {
1694 return true;
1695 };
1696 let (sub_u, sup_u) = (sub_ty.to_union(), sup_ty.to_union());
1697 if has_nominal_type(&sub_u) || has_nominal_type(&sup_u) {
1698 return true;
1699 }
1700 sup_u.is_subtype_structural(&sub_u)
1703 });
1704 params_ok
1705 && (sub.return_type.is_mixed()
1706 || sup.return_type.is_mixed()
1707 || has_nominal_type(&sub.return_type)
1708 || has_nominal_type(&sup.return_type)
1709 || sub.return_type.is_subtype_structural(&sup.return_type))
1710 }
1711 }
1712 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1714 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1716 fqcn.as_ref().eq_ignore_ascii_case("closure")
1717 }
1718 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1719 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1721 fqcn.as_ref().eq_ignore_ascii_case("closure")
1722 }
1723 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1725 fqcn.as_ref().eq_ignore_ascii_case("closure")
1726 }
1727
1728 (
1736 Atomic::TIntersection { parts: sub_parts },
1737 Atomic::TIntersection { parts: sup_parts },
1738 ) => sup_parts.iter().all(|sup_part| {
1739 sub_parts
1740 .iter()
1741 .any(|sub_part| sub_part.is_subtype_structural(sup_part))
1742 }),
1743
1744 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1746 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1747 }
1748 (Atomic::TNonEmptyList { value }, Atomic::TArray { key, value: av }) => {
1749 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1750 }
1751 (Atomic::TNonEmptyList { value }, Atomic::TNonEmptyArray { key, value: av }) => {
1752 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1753 }
1754 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1755 value.is_subtype_structural(lv)
1756 }
1757 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1759 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1760 && av.is_subtype_structural(lv)
1761 }
1762 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1763 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1764 && av.is_subtype_structural(lv)
1765 }
1766 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1767 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1768 && av.is_subtype_structural(lv)
1769 }
1770 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1771 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1772 && av.is_subtype_structural(lv)
1773 }
1774 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1776 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1777 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1778 }
1779
1780 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1782 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1783 }
1784
1785 (Atomic::TKeyedArray { properties, .. }, Atomic::TArray { key, value }) => {
1794 properties.iter().all(|(prop_key, prop)| {
1795 let key_atomic = match prop_key {
1796 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1797 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1798 };
1799 if !Type::single(key_atomic).is_subtype_structural(key) {
1800 return false; }
1802 let has_named_obj = prop.ty.types.iter().any(|a| {
1804 matches!(
1805 a,
1806 Atomic::TNamedObject { .. }
1807 | Atomic::TSelf { .. }
1808 | Atomic::TStaticObject { .. }
1809 | Atomic::TClosure { .. }
1810 | Atomic::TTemplateParam { .. }
1811 )
1812 });
1813 has_named_obj || prop.ty.is_subtype_structural(value)
1814 })
1815 }
1816 (
1817 Atomic::TKeyedArray {
1818 properties,
1819 is_open,
1820 ..
1821 },
1822 Atomic::TNonEmptyArray { key, value },
1823 ) => {
1824 (*is_open || properties.iter().any(|(_, p)| !p.optional))
1825 && properties.iter().all(|(prop_key, prop)| {
1826 let key_atomic = match prop_key {
1827 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1828 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1829 };
1830 if !Type::single(key_atomic).is_subtype_structural(key) {
1831 return false;
1832 }
1833 let has_named_obj = prop.ty.types.iter().any(|a| {
1834 matches!(
1835 a,
1836 Atomic::TNamedObject { .. }
1837 | Atomic::TSelf { .. }
1838 | Atomic::TStaticObject { .. }
1839 | Atomic::TClosure { .. }
1840 | Atomic::TTemplateParam { .. }
1841 )
1842 });
1843 has_named_obj || prop.ty.is_subtype_structural(value)
1844 })
1845 }
1846
1847 (
1849 Atomic::TKeyedArray {
1850 properties,
1851 is_list,
1852 ..
1853 },
1854 Atomic::TList { value: lv },
1855 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1856 (
1857 Atomic::TKeyedArray {
1858 properties,
1859 is_list,
1860 ..
1861 },
1862 Atomic::TNonEmptyList { value: lv },
1863 ) => {
1864 *is_list
1865 && !properties.is_empty()
1866 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1867 }
1868
1869 (
1874 Atomic::TKeyedArray {
1875 properties: sub_props,
1876 is_open: sub_open,
1877 ..
1878 },
1879 Atomic::TKeyedArray {
1880 properties: sup_props,
1881 is_open: sup_open,
1882 ..
1883 },
1884 ) => {
1885 let keys_satisfied = sup_props
1886 .iter()
1887 .all(|(key, sup_prop)| match sub_props.get(key) {
1888 Some(sub_prop) => {
1889 if !sup_prop.optional && sub_prop.optional {
1893 return false;
1894 }
1895 let has_named_obj = sup_prop.ty.types.iter().any(|a| {
1896 matches!(
1897 a,
1898 Atomic::TNamedObject { .. }
1899 | Atomic::TSelf { .. }
1900 | Atomic::TStaticObject { .. }
1901 | Atomic::TClosure { .. }
1902 | Atomic::TTemplateParam { .. }
1903 )
1904 });
1905 has_named_obj || sub_prop.ty.is_subtype_structural(&sup_prop.ty)
1906 }
1907 None => sup_prop.optional || *sub_open,
1908 });
1909 let no_undeclared_extras =
1910 *sup_open || sub_props.keys().all(|k| sup_props.contains_key(k));
1911 keys_satisfied && no_undeclared_extras
1912 }
1913
1914 _ => false,
1915 }
1916}
1917
1918fn type_params_compatible(sub: &[Type], sup: &[Type]) -> bool {
1924 if sub.len() != sup.len() {
1925 return false;
1926 }
1927 sub.iter()
1928 .zip(sup.iter())
1929 .all(|(a, b)| a == b || (is_empty_array_literal(a) && is_array_like(b)))
1930}
1931
1932fn is_empty_array_literal(t: &Type) -> bool {
1935 !t.types.is_empty()
1936 && t.types.iter().all(
1937 |atom| matches!(atom, Atomic::TKeyedArray { properties, .. } if properties.is_empty()),
1938 )
1939}
1940
1941fn is_array_like(t: &Type) -> bool {
1943 !t.types.is_empty() && t.types.iter().all(|atom| atom.is_array())
1944}
1945
1946#[cfg(test)]
1951mod tests {
1952 use std::sync::Arc;
1953
1954 use super::*;
1955
1956 fn conditional(
1957 param_name: Option<Name>,
1958 subject: Type,
1959 if_true: Type,
1960 if_false: Type,
1961 ) -> Atomic {
1962 Atomic::TConditional {
1963 data: Box::new(crate::atomic::ConditionalData {
1964 param_name,
1965 subject,
1966 if_true,
1967 if_false,
1968 }),
1969 }
1970 }
1971
1972 #[test]
1973 fn single_is_single() {
1974 let u = Type::single(Atomic::TString);
1975 assert!(u.is_single());
1976 assert!(!u.is_nullable());
1977 }
1978
1979 #[test]
1980 fn nullable_has_null() {
1981 let u = Type::nullable(Atomic::TString);
1982 assert!(u.is_nullable());
1983 assert_eq!(u.types.len(), 2);
1984 }
1985
1986 #[test]
1987 fn add_type_deduplicates() {
1988 let mut u = Type::single(Atomic::TString);
1989 u.add_type(Atomic::TString);
1990 assert_eq!(u.types.len(), 1);
1991 }
1992
1993 #[test]
1994 fn array_key_is_int_string() {
1995 let k = Type::array_key();
1996 assert!(k.is_array_key());
1997 assert_eq!(k.types.len(), 2);
1998 }
1999
2000 #[test]
2001 fn is_array_key_false_for_plain_int() {
2002 assert!(!Type::int().is_array_key());
2003 }
2004
2005 #[test]
2006 fn is_array_key_false_for_mixed() {
2007 assert!(!Type::mixed().is_array_key());
2008 }
2009
2010 #[test]
2011 fn is_array_key_false_for_int_string_null() {
2012 let mut u = Type::array_key();
2013 u.add_type(Atomic::TNull);
2014 assert!(!u.is_array_key());
2015 }
2016
2017 #[test]
2018 fn add_type_literal_subsumed_by_base() {
2019 let mut u = Type::single(Atomic::TInt);
2020 u.add_type(Atomic::TLiteralInt(42));
2021 assert_eq!(u.types.len(), 1);
2022 assert!(matches!(u.types[0], Atomic::TInt));
2023 }
2024
2025 #[test]
2026 fn true_then_false_merges_to_bool() {
2027 let mut u = Type::single(Atomic::TTrue);
2028 u.add_type(Atomic::TFalse);
2029 assert_eq!(u.types.len(), 1);
2030 assert!(matches!(u.types[0], Atomic::TBool));
2031 }
2032
2033 #[test]
2034 fn false_then_true_merges_to_bool() {
2035 let mut u = Type::single(Atomic::TFalse);
2036 u.add_type(Atomic::TTrue);
2037 assert_eq!(u.types.len(), 1);
2038 assert!(matches!(u.types[0], Atomic::TBool));
2039 }
2040
2041 #[test]
2042 fn true_alone_stays_true() {
2043 let u = Type::single(Atomic::TTrue);
2044 assert_eq!(u.types.len(), 1);
2045 assert!(matches!(u.types[0], Atomic::TTrue));
2046 }
2047
2048 #[test]
2049 fn true_false_merge_preserves_other_union_members() {
2050 let mut u = Type::single(Atomic::TTrue);
2051 u.add_type(Atomic::TNull);
2052 u.add_type(Atomic::TFalse);
2053 assert_eq!(u.types.len(), 2);
2054 assert!(u.contains(|t| matches!(t, Atomic::TBool)));
2055 assert!(u.contains(|t| matches!(t, Atomic::TNull)));
2056 }
2057
2058 #[test]
2059 fn add_type_base_widens_literals() {
2060 let mut u = Type::single(Atomic::TLiteralInt(1));
2061 u.add_type(Atomic::TLiteralInt(2));
2062 u.add_type(Atomic::TInt);
2063 assert_eq!(u.types.len(), 1);
2064 assert!(matches!(u.types[0], Atomic::TInt));
2065 }
2066
2067 #[test]
2068 fn mixed_subsumes_everything() {
2069 let mut u = Type::single(Atomic::TString);
2070 u.add_type(Atomic::TMixed);
2071 assert_eq!(u.types.len(), 1);
2072 assert!(u.is_mixed());
2073 }
2074
2075 #[test]
2076 fn remove_null() {
2077 let u = Type::nullable(Atomic::TString);
2078 let narrowed = u.remove_null();
2079 assert!(!narrowed.is_nullable());
2080 assert_eq!(narrowed.types.len(), 1);
2081 }
2082
2083 #[test]
2084 fn narrow_to_truthy_removes_null_false() {
2085 let mut u = Type::empty();
2086 u.add_type(Atomic::TString);
2087 u.add_type(Atomic::TNull);
2088 u.add_type(Atomic::TFalse);
2089 let truthy = u.narrow_to_truthy();
2090 assert!(!truthy.is_nullable());
2091 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
2092 }
2093
2094 #[test]
2095 fn merge_combines_types() {
2096 let a = Type::single(Atomic::TString);
2097 let b = Type::single(Atomic::TInt);
2098 let merged = Type::merge(&a, &b);
2099 assert_eq!(merged.types.len(), 2);
2100 }
2101
2102 #[test]
2103 fn intersect_keeps_narrower_side_not_self() {
2104 let int_ty = Type::single(Atomic::TInt);
2108 let mut literals = Type::empty();
2109 literals.add_type(Atomic::TLiteralInt(1));
2110 literals.add_type(Atomic::TLiteralInt(2));
2111
2112 let narrowed = int_ty.intersect_with(&literals);
2113 assert_eq!(narrowed.types.len(), 2);
2114 assert!(narrowed.contains(|t| matches!(t, Atomic::TLiteralInt(1))));
2115 assert!(narrowed.contains(|t| matches!(t, Atomic::TLiteralInt(2))));
2116 assert!(!narrowed.contains(|t| matches!(t, Atomic::TInt)));
2117 }
2118
2119 #[test]
2120 fn subtype_literal_int_under_int() {
2121 let sub = Type::single(Atomic::TLiteralInt(5));
2122 let sup = Type::single(Atomic::TInt);
2123 assert!(sub.is_subtype_structural(&sup));
2124 }
2125
2126 #[test]
2127 fn subtype_never_is_bottom() {
2128 let never = Type::never();
2129 let string = Type::single(Atomic::TString);
2130 assert!(never.is_subtype_structural(&string));
2131 }
2132
2133 #[test]
2134 fn subtype_everything_under_mixed() {
2135 let string = Type::single(Atomic::TString);
2136 let mixed = Type::mixed();
2137 assert!(string.is_subtype_structural(&mixed));
2138 }
2139
2140 #[test]
2141 fn template_substitution() {
2142 let mut bindings = FxHashMap::default();
2143 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
2144
2145 let tmpl = Type::single(Atomic::TTemplateParam {
2146 name: Name::new("T"),
2147 as_type: Box::new(Type::mixed()),
2148 defining_entity: Name::new("MyClass"),
2149 });
2150
2151 let resolved = tmpl.substitute_templates(&bindings);
2152 assert_eq!(resolved.types.len(), 1);
2153 assert!(matches!(resolved.types[0], Atomic::TString));
2154 }
2155
2156 #[test]
2157 fn intersection_is_object() {
2158 let parts = vec![
2159 Type::single(Atomic::TNamedObject {
2160 fqcn: Name::new("Iterator"),
2161 type_params: empty_type_params(),
2162 }),
2163 Type::single(Atomic::TNamedObject {
2164 fqcn: Name::new("Countable"),
2165 type_params: empty_type_params(),
2166 }),
2167 ];
2168 let atomic = Atomic::TIntersection {
2169 parts: vec_to_type_params(parts),
2170 };
2171 assert!(atomic.is_object());
2172 assert!(!atomic.can_be_falsy());
2173 assert!(atomic.can_be_truthy());
2174 }
2175
2176 #[test]
2177 fn intersection_display_two_parts() {
2178 let parts = vec![
2179 Type::single(Atomic::TNamedObject {
2180 fqcn: Name::new("Iterator"),
2181 type_params: empty_type_params(),
2182 }),
2183 Type::single(Atomic::TNamedObject {
2184 fqcn: Name::new("Countable"),
2185 type_params: empty_type_params(),
2186 }),
2187 ];
2188 let u = Type::single(Atomic::TIntersection {
2189 parts: vec_to_type_params(parts),
2190 });
2191 assert_eq!(format!("{u}"), "Iterator&Countable");
2192 }
2193
2194 #[test]
2195 fn intersection_display_three_parts() {
2196 let parts = vec![
2197 Type::single(Atomic::TNamedObject {
2198 fqcn: Name::new("A"),
2199 type_params: empty_type_params(),
2200 }),
2201 Type::single(Atomic::TNamedObject {
2202 fqcn: Name::new("B"),
2203 type_params: empty_type_params(),
2204 }),
2205 Type::single(Atomic::TNamedObject {
2206 fqcn: Name::new("C"),
2207 type_params: empty_type_params(),
2208 }),
2209 ];
2210 let u = Type::single(Atomic::TIntersection {
2211 parts: vec_to_type_params(parts),
2212 });
2213 assert_eq!(format!("{u}"), "A&B&C");
2214 }
2215
2216 #[test]
2217 fn intersection_in_nullable_union_display() {
2218 let intersection = Atomic::TIntersection {
2219 parts: vec_to_type_params(vec![
2220 Type::single(Atomic::TNamedObject {
2221 fqcn: Name::new("Iterator"),
2222 type_params: empty_type_params(),
2223 }),
2224 Type::single(Atomic::TNamedObject {
2225 fqcn: Name::new("Countable"),
2226 type_params: empty_type_params(),
2227 }),
2228 ]),
2229 };
2230 let mut u = Type::single(intersection);
2231 u.add_type(Atomic::TNull);
2232 assert!(u.is_nullable());
2233 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
2234 }
2235
2236 fn t_param(name: &str) -> Type {
2239 Type::single(Atomic::TTemplateParam {
2240 name: Name::new(name),
2241 as_type: Box::new(Type::mixed()),
2242 defining_entity: Name::new("Fn"),
2243 })
2244 }
2245
2246 fn bindings_t_string() -> FxHashMap<Name, Type> {
2247 let mut b = FxHashMap::default();
2248 b.insert(Name::new("T"), Type::single(Atomic::TString));
2249 b
2250 }
2251
2252 #[test]
2253 fn substitute_non_empty_array_key_and_value() {
2254 let ty = Type::single(Atomic::TNonEmptyArray {
2255 key: Box::new(t_param("T")),
2256 value: Box::new(t_param("T")),
2257 });
2258 let result = ty.substitute_templates(&bindings_t_string());
2259 assert_eq!(result.types.len(), 1);
2260 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
2261 panic!("expected TNonEmptyArray");
2262 };
2263 assert!(matches!(key.types[0], Atomic::TString));
2264 assert!(matches!(value.types[0], Atomic::TString));
2265 }
2266
2267 #[test]
2268 fn substitute_non_empty_list_value() {
2269 let ty = Type::single(Atomic::TNonEmptyList {
2270 value: Box::new(t_param("T")),
2271 });
2272 let result = ty.substitute_templates(&bindings_t_string());
2273 let Atomic::TNonEmptyList { value } = &result.types[0] else {
2274 panic!("expected TNonEmptyList");
2275 };
2276 assert!(matches!(value.types[0], Atomic::TString));
2277 }
2278
2279 #[test]
2280 fn substitute_keyed_array_property_types() {
2281 use crate::atomic::{ArrayKey, KeyedProperty};
2282 use indexmap::IndexMap;
2283 let mut props = IndexMap::new();
2284 props.insert(
2285 ArrayKey::String(Arc::from("name")),
2286 KeyedProperty {
2287 ty: t_param("T"),
2288 optional: false,
2289 },
2290 );
2291 props.insert(
2292 ArrayKey::String(Arc::from("tag")),
2293 KeyedProperty {
2294 ty: t_param("T"),
2295 optional: true,
2296 },
2297 );
2298 let ty = Type::single(Atomic::TKeyedArray {
2299 properties: Box::new(props),
2300 is_open: true,
2301 is_list: false,
2302 });
2303 let result = ty.substitute_templates(&bindings_t_string());
2304 let Atomic::TKeyedArray {
2305 properties,
2306 is_open,
2307 is_list,
2308 } = &result.types[0]
2309 else {
2310 panic!("expected TKeyedArray");
2311 };
2312 assert!(is_open);
2313 assert!(!is_list);
2314 assert!(matches!(
2315 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
2316 Atomic::TString
2317 ));
2318 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
2319 assert!(matches!(
2320 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
2321 Atomic::TString
2322 ));
2323 }
2324
2325 #[test]
2326 fn substitute_callable_params_and_return() {
2327 use crate::atomic::FnParam;
2328 let ty = Type::single(Atomic::TCallable {
2329 params: Some(Box::new([FnParam {
2330 name: Name::new("x"),
2331 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2332 out_ty: None,
2333 default: None,
2334 is_variadic: false,
2335 is_byref: false,
2336 is_optional: false,
2337 }])),
2338 return_type: Some(Box::new(t_param("T"))),
2339 });
2340 let result = ty.substitute_templates(&bindings_t_string());
2341 let Atomic::TCallable {
2342 params,
2343 return_type,
2344 } = &result.types[0]
2345 else {
2346 panic!("expected TCallable");
2347 };
2348 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
2349 let param_union = param_ty.to_union();
2350 assert!(matches!(param_union.types[0], Atomic::TString));
2351 let ret = return_type.as_ref().unwrap();
2352 assert!(matches!(ret.types[0], Atomic::TString));
2353 }
2354
2355 #[test]
2356 fn substitute_callable_bare_no_panic() {
2357 let ty = Type::single(Atomic::TCallable {
2359 params: None,
2360 return_type: None,
2361 });
2362 let result = ty.substitute_templates(&bindings_t_string());
2363 assert!(matches!(
2364 result.types[0],
2365 Atomic::TCallable {
2366 params: None,
2367 return_type: None
2368 }
2369 ));
2370 }
2371
2372 #[test]
2373 fn substitute_closure_params_return_and_this() {
2374 use crate::atomic::FnParam;
2375 let ty = Type::single(Atomic::TClosure {
2376 data: Box::new(crate::atomic::ClosureData {
2377 params: Box::new([FnParam {
2378 name: Name::new("a"),
2379 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2380 out_ty: None,
2381 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2382 is_variadic: true,
2383 is_byref: true,
2384 is_optional: true,
2385 }]),
2386 return_type: t_param("T"),
2387 this_type: Some(t_param("T")),
2388 }),
2389 });
2390 let result = ty.substitute_templates(&bindings_t_string());
2391 let Atomic::TClosure { data } = &result.types[0] else {
2392 panic!("expected TClosure");
2393 };
2394 let (params, return_type, this_type) = (&data.params, &data.return_type, &data.this_type);
2395 let p = ¶ms[0];
2396 let ty_union = p.ty.as_ref().unwrap().to_union();
2397 let default_union = p.default.as_ref().unwrap().to_union();
2398 assert!(matches!(ty_union.types[0], Atomic::TString));
2399 assert!(matches!(default_union.types[0], Atomic::TString));
2400 assert!(p.is_variadic);
2402 assert!(p.is_byref);
2403 assert!(p.is_optional);
2404 assert!(matches!(return_type.types[0], Atomic::TString));
2405 assert!(matches!(
2406 this_type.as_ref().unwrap().types[0],
2407 Atomic::TString
2408 ));
2409 }
2410
2411 #[test]
2412 fn substitute_conditional_all_branches() {
2413 let ty = Type::single(conditional(
2414 None,
2415 t_param("T"),
2416 t_param("T"),
2417 Type::single(Atomic::TInt),
2418 ));
2419 let result = ty.substitute_templates(&bindings_t_string());
2420 let Atomic::TConditional { data } = &result.types[0] else {
2421 panic!("expected TConditional");
2422 };
2423 let (subject, if_true, if_false) = (&data.subject, &data.if_true, &data.if_false);
2424 assert!(matches!(subject.types[0], Atomic::TString));
2425 assert!(matches!(if_true.types[0], Atomic::TString));
2426 assert!(matches!(if_false.types[0], Atomic::TInt));
2427 }
2428
2429 #[test]
2430 fn resolve_conditional_is_null_non_null_arg() {
2431 let ty = Type::single(conditional(
2432 Some(Name::new("x")),
2433 Type::single(Atomic::TNull),
2434 Type::single(Atomic::TInt),
2435 Type::single(Atomic::TString),
2436 ));
2437 let result = ty.resolve_conditional_returns(|name| {
2438 if name == "x" {
2439 Some(Type::single(Atomic::TString)) } else {
2441 None
2442 }
2443 });
2444 assert!(result.types.len() == 1);
2445 assert!(matches!(result.types[0], Atomic::TString));
2446 }
2447
2448 #[test]
2449 fn resolve_conditional_is_null_null_arg() {
2450 let ty = Type::single(conditional(
2451 Some(Name::new("x")),
2452 Type::single(Atomic::TNull),
2453 Type::single(Atomic::TInt),
2454 Type::single(Atomic::TString),
2455 ));
2456 let result = ty.resolve_conditional_returns(|name| {
2457 if name == "x" {
2458 Some(Type::single(Atomic::TNull)) } else {
2460 None
2461 }
2462 });
2463 assert!(result.types.len() == 1);
2464 assert!(matches!(result.types[0], Atomic::TInt));
2465 }
2466
2467 #[test]
2468 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
2469 let mut nullable_str = Type::single(Atomic::TString);
2470 nullable_str.add_type(Atomic::TNull);
2471 let ty = Type::single(conditional(
2472 Some(Name::new("x")),
2473 Type::single(Atomic::TNull),
2474 Type::single(Atomic::TInt),
2475 Type::single(Atomic::TString),
2476 ));
2477 let result = ty.resolve_conditional_returns(|name| {
2478 if name == "x" {
2479 Some(nullable_str.clone())
2480 } else {
2481 None
2482 }
2483 });
2484 assert_eq!(result.types.len(), 2);
2486 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2487 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2488 }
2489
2490 #[test]
2491 fn resolve_conditional_nested_widens_inner_branch() {
2492 let inner = Type::single(conditional(
2495 Some(Name::new("x")),
2496 Type::single(Atomic::TString),
2497 Type::single(Atomic::TString),
2498 Type::single(Atomic::TFloat),
2499 ));
2500 let ty = Type::single(conditional(
2501 Some(Name::new("x")),
2502 Type::single(Atomic::TNull),
2503 Type::single(Atomic::TInt),
2504 inner,
2505 ));
2506 let result = ty.resolve_conditional_returns(|_| None);
2508 assert!(
2509 result
2510 .types
2511 .iter()
2512 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2513 "no TConditional should survive: {:?}",
2514 result.types
2515 );
2516 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2517 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2518 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
2519 }
2520
2521 #[test]
2522 fn resolve_conditional_nested_resolves_inner_branch() {
2523 let inner = Type::single(conditional(
2527 Some(Name::new("x")),
2528 Type::single(Atomic::TString),
2529 Type::single(Atomic::TString),
2530 Type::single(Atomic::TFloat),
2531 ));
2532 let ty = Type::single(conditional(
2533 Some(Name::new("x")),
2534 Type::single(Atomic::TNull),
2535 Type::single(Atomic::TInt),
2536 inner,
2537 ));
2538 let result = ty.resolve_conditional_returns(|name| {
2540 if name == "x" {
2541 Some(Type::single(Atomic::TString))
2542 } else {
2543 None
2544 }
2545 });
2546 assert!(
2547 result
2548 .types
2549 .iter()
2550 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2551 "no TConditional should survive: {:?}",
2552 result.types
2553 );
2554 assert_eq!(result.types.len(), 1);
2555 assert!(matches!(result.types[0], Atomic::TString));
2556 }
2557
2558 #[test]
2559 fn substitute_intersection_parts() {
2560 let ty = Type::single(Atomic::TIntersection {
2561 parts: vec_to_type_params(vec![
2562 Type::single(Atomic::TNamedObject {
2563 fqcn: Name::new("Countable"),
2564 type_params: empty_type_params(),
2565 }),
2566 t_param("T"),
2567 ]),
2568 });
2569 let result = ty.substitute_templates(&bindings_t_string());
2570 let Atomic::TIntersection { parts } = &result.types[0] else {
2571 panic!("expected TIntersection");
2572 };
2573 assert_eq!(parts.len(), 2);
2574 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
2575 assert!(matches!(parts[1].types[0], Atomic::TString));
2576 }
2577
2578 #[test]
2579 fn substitute_no_template_params_identity() {
2580 let ty = Type::single(Atomic::TInt);
2581 let result = ty.substitute_templates(&bindings_t_string());
2582 assert!(matches!(result.types[0], Atomic::TInt));
2583 }
2584}