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 { properties, .. } = &atomic {
346 if properties.is_empty() {
347 for existing in &self.types {
348 match existing {
349 Atomic::TArray { .. }
350 | Atomic::TNonEmptyArray { .. }
351 | Atomic::TList { .. }
352 | Atomic::TNonEmptyList { .. } => {
353 return; }
355 _ => {}
356 }
357 }
358 }
359 }
360
361 let is_generic_array_or_list = matches!(
363 &atomic,
364 Atomic::TArray { .. }
365 | Atomic::TNonEmptyArray { .. }
366 | Atomic::TList { .. }
367 | Atomic::TNonEmptyList { .. }
368 );
369 if is_generic_array_or_list {
370 self.types.retain(|t| {
371 if let Atomic::TKeyedArray { properties, .. } = t {
372 !properties.is_empty()
373 } else {
374 true
375 }
376 });
377 }
378
379 self.types.push(atomic);
380 }
381
382 pub fn remove_null(&self) -> Type {
386 self.filter(|t| !matches!(t, Atomic::TNull))
387 }
388
389 pub fn remove_false(&self) -> Type {
392 let mut result = self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool));
393 if self.types.iter().any(|t| matches!(t, Atomic::TBool)) {
394 result.add_type(Atomic::TTrue);
395 }
396 result
397 }
398
399 pub fn core_type(&self) -> Type {
401 self.remove_null().remove_false()
402 }
403
404 pub fn narrow_to_truthy(&self) -> Type {
406 if self.is_mixed_not_template() {
407 return Type::mixed();
408 }
409 let mut result = Type::empty();
410 result.from_docblock = self.from_docblock;
411 for t in &self.types {
412 match t {
413 Atomic::TTemplateParam { .. } => result.add_type(t.clone()),
417 Atomic::TLiteralInt(0)
419 | Atomic::TLiteralFloat(0, 0)
420 | Atomic::TNull
421 | Atomic::TFalse => {}
422 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => {}
423 Atomic::TBool => result.add_type(Atomic::TTrue),
425 Atomic::TArray { key, value } => result.add_type(Atomic::TNonEmptyArray {
427 key: key.clone(),
428 value: value.clone(),
429 }),
430 Atomic::TList { value } => result.add_type(Atomic::TNonEmptyList {
431 value: value.clone(),
432 }),
433 Atomic::TString => result.add_type(Atomic::TNonEmptyString),
437 Atomic::TNonNegativeInt => result.add_type(Atomic::TPositiveInt),
442 Atomic::TIntRange { min: Some(0), max } if max.is_none_or(|m| m >= 1) => {
443 let atom = if max.is_none() {
444 Atomic::TPositiveInt
445 } else {
446 Atomic::TIntRange {
447 min: Some(1),
448 max: *max,
449 }
450 };
451 result.add_type(atom);
452 }
453 Atomic::TIntRange { min, max: Some(0) } => {
455 let atom = match min {
456 None => Atomic::TNegativeInt,
457 Some(n) if *n <= -1 => Atomic::TIntRange {
458 min: *min,
459 max: Some(-1),
460 },
461 _ => continue, };
463 result.add_type(atom);
464 }
465 t if !t.can_be_truthy() => {}
467 _ => result.add_type(t.clone()),
468 }
469 }
470 result
471 }
472
473 pub fn narrow_to_falsy(&self) -> Type {
475 if self.is_mixed_not_template() {
476 return Type::from_vec(vec![
477 Atomic::TNull,
478 Atomic::TFalse,
479 Atomic::TLiteralInt(0),
480 Atomic::TLiteralString("".into()),
481 ]);
482 }
483 let mut result = Type::empty();
484 result.from_docblock = self.from_docblock;
485 for t in &self.types {
486 match t {
487 Atomic::TTemplateParam { .. } => result.add_type(t.clone()),
492 Atomic::TBool => result.add_type(Atomic::TFalse),
494 Atomic::TInt => result.add_type(Atomic::TLiteralInt(0)),
496 Atomic::TFloat => result.add_type(Atomic::TLiteralFloat(0, 0)),
498 Atomic::TString => {
500 result.add_type(Atomic::TLiteralString("".into()));
501 result.add_type(Atomic::TLiteralString("0".into()));
502 }
503 Atomic::TNumericString => result.add_type(Atomic::TLiteralString("0".into())),
505 Atomic::TNonNegativeInt => result.add_type(Atomic::TLiteralInt(0)),
507 Atomic::TIntRange {
509 min: Some(0),
510 max: Some(_) | None,
511 } => result.add_type(Atomic::TLiteralInt(0)),
512 Atomic::TIntRange { max: Some(0), .. } => result.add_type(Atomic::TLiteralInt(0)),
514 t if !t.can_be_falsy() => {} _ => result.add_type(t.clone()),
516 }
517 }
518 result
519 }
520
521 pub fn narrow_instanceof(&self, class: &str) -> Type {
527 let narrowed_ty = Atomic::TNamedObject {
528 fqcn: class.into(),
529 type_params: empty_type_params(),
530 };
531 let has_object = self.types.iter().any(|t| {
533 matches!(
534 t,
535 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
537 });
538 if has_object || self.is_empty() {
539 Type::single(narrowed_ty)
540 } else {
541 Type::single(narrowed_ty)
544 }
545 }
546
547 pub fn narrow_to_string(&self) -> Type {
551 self.filter_replacing(
552 |t| t.is_string() || matches!(t, Atomic::TTemplateParam { .. }),
553 |t| matches!(t, Atomic::TMixed | Atomic::TScalar),
554 Atomic::TString,
555 )
556 }
557
558 pub fn narrow_to_int(&self) -> Type {
560 self.filter_replacing(
561 |t| t.is_int() || matches!(t, Atomic::TTemplateParam { .. }),
562 |t| matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric),
563 Atomic::TInt,
564 )
565 }
566
567 pub fn narrow_to_float(&self) -> Type {
569 self.filter_replacing(
570 |t| {
571 matches!(
572 t,
573 Atomic::TFloat
574 | Atomic::TIntegralFloat
575 | Atomic::TLiteralFloat(..)
576 | Atomic::TTemplateParam { .. }
577 )
578 },
579 |t| matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric),
580 Atomic::TFloat,
581 )
582 }
583
584 pub fn narrow_to_bool(&self) -> Type {
586 self.filter_replacing(
587 |t| {
588 matches!(
589 t,
590 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TTemplateParam { .. }
591 )
592 },
593 |t| matches!(t, Atomic::TMixed | Atomic::TScalar),
594 Atomic::TBool,
595 )
596 }
597
598 pub fn narrow_to_null(&self) -> Type {
600 self.filter_replacing(
601 |t| matches!(t, Atomic::TNull | Atomic::TTemplateParam { .. }),
602 |t| matches!(t, Atomic::TMixed),
603 Atomic::TNull,
604 )
605 }
606
607 pub fn narrow_to_array(&self) -> Type {
609 self.filter_replacing(
610 |t| t.is_array() || matches!(t, Atomic::TTemplateParam { .. }),
611 |t| matches!(t, Atomic::TMixed),
612 Atomic::TArray {
613 key: Box::new(Type::mixed()),
614 value: Box::new(Type::mixed()),
615 },
616 )
617 }
618
619 pub fn narrow_to_non_empty_collection(&self) -> Type {
621 let mut out = Type::empty();
622 out.from_docblock = self.from_docblock;
623 for t in &self.types {
624 match t {
625 Atomic::TArray { key, value } => out.add_type(Atomic::TNonEmptyArray {
626 key: key.clone(),
627 value: value.clone(),
628 }),
629 Atomic::TList { value } => out.add_type(Atomic::TNonEmptyList {
630 value: value.clone(),
631 }),
632 _ => out.add_type(t.clone()),
633 }
634 }
635 out
636 }
637
638 pub fn narrow_to_empty_collection(&self) -> Type {
643 self.filter(|t| {
644 !matches!(
645 t,
646 Atomic::TNonEmptyArray { .. } | Atomic::TNonEmptyList { .. }
647 )
648 })
649 }
650
651 pub fn narrow_to_list(&self) -> Type {
663 let mut out = Type::empty();
664 out.from_docblock = self.from_docblock;
665 for t in &self.types {
666 match t {
667 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => out.add_type(t.clone()),
668 Atomic::TArray { key, value } if matches!(key.types.as_slice(), [Atomic::TInt]) => {
669 out.add_type(Atomic::TList {
670 value: value.clone(),
671 });
672 }
673 Atomic::TNonEmptyArray { key, value }
674 if matches!(key.types.as_slice(), [Atomic::TInt]) =>
675 {
676 out.add_type(Atomic::TNonEmptyList {
677 value: value.clone(),
678 });
679 }
680 Atomic::TKeyedArray { is_list: true, .. } => out.add_type(t.clone()),
681 Atomic::TMixed => out.add_type(Atomic::TList {
682 value: Box::new(Type::mixed()),
683 }),
684 _ => {}
685 }
686 }
687 if out.is_empty() {
688 self.filter(|t| matches!(t, Atomic::TList { .. } | Atomic::TNonEmptyList { .. }))
689 } else {
690 out
691 }
692 }
693
694 pub fn narrow_to_object(&self) -> Type {
699 let mut out = Type::empty();
700 for t in &self.types {
701 if matches!(t, Atomic::TMixed) {
702 out.add_type(Atomic::TObject);
703 } else if t.is_object() || matches!(t, Atomic::TTemplateParam { .. }) {
704 out.add_type(t.clone());
705 }
706 }
707 if out.types.is_empty() {
708 self.filter(|t| t.is_object())
709 } else {
710 out
711 }
712 }
713
714 pub fn narrow_to_callable(&self) -> Type {
721 self.filter(|t| {
722 t.is_callable()
723 || t.is_string()
724 || t.is_array()
725 || t.is_object()
726 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
727 })
728 }
729
730 pub fn narrow_to_scalar(&self) -> Type {
732 self.filter_replacing(
733 |t| {
734 t.is_string()
735 || t.is_int()
736 || matches!(
737 t,
738 Atomic::TFloat
739 | Atomic::TIntegralFloat
740 | Atomic::TLiteralFloat(..)
741 | Atomic::TBool
742 | Atomic::TTrue
743 | Atomic::TFalse
744 | Atomic::TScalar
745 | Atomic::TNumeric
746 | Atomic::TNumericString
747 | Atomic::TTemplateParam { .. }
748 )
749 },
750 |t| matches!(t, Atomic::TMixed),
751 Atomic::TScalar,
752 )
753 }
754
755 pub fn narrow_to_iterable(&self) -> Type {
758 self.filter(|t| {
759 t.is_array()
760 || t.is_object()
761 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
762 })
763 }
764
765 pub fn narrow_to_countable(&self) -> Type {
768 self.filter(|t| {
769 t.is_array()
770 || t.is_object()
771 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
772 })
773 }
774
775 pub fn narrow_to_resource(&self) -> Type {
779 self.filter(|t| matches!(t, Atomic::TMixed))
781 }
782
783 pub fn narrow_to_class_string(&self) -> Type {
788 let mut out = Type::empty();
789 out.from_docblock = self.from_docblock;
790 for t in &self.types {
791 match t {
792 Atomic::TClassString(_) => out.add_type(t.clone()),
793 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
794 out.add_type(Atomic::TClassString(None));
795 }
796 _ => {}
797 }
798 }
799 out
800 }
801
802 pub fn narrow_to_interface_string(&self) -> Type {
807 let mut out = Type::empty();
808 out.from_docblock = self.from_docblock;
809 for t in &self.types {
810 match t {
811 Atomic::TInterfaceString(_) => out.add_type(t.clone()),
812 Atomic::TClassString(name) => {
816 out.add_type(Atomic::TInterfaceString(*name));
817 }
818 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
819 out.add_type(Atomic::TInterfaceString(None));
820 }
821 _ => {}
822 }
823 }
824 out
825 }
826
827 pub fn merge(a: &Type, b: &Type) -> Type {
832 if b.types.is_empty() {
834 let mut result = a.clone();
835 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
836 return result;
837 }
838 if a.types.is_empty() {
840 let mut result = b.clone();
841 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
842 return result;
843 }
844 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
846 let mut result = a.clone();
847 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
848 return result;
849 }
850 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
852 return Type {
853 types: smallvec::smallvec![Atomic::TMixed],
854 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
855 from_docblock: a.from_docblock || b.from_docblock,
856 };
857 }
858 let mut result = a.clone();
859 result.merge_with(b);
860 result
861 }
862
863 pub fn merge_with(&mut self, other: &Type) {
865 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
866 self.possibly_undefined |= other.possibly_undefined;
867 return;
868 }
869 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
870 self.types.clear();
871 self.types.push(Atomic::TMixed);
872 self.possibly_undefined |= other.possibly_undefined;
873 return;
874 }
875 for atomic in &other.types {
876 self.add_type(atomic.clone());
877 }
878 self.possibly_undefined |= other.possibly_undefined;
879 }
880
881 pub fn intersect_with(&self, other: &Type) -> Type {
885 if self.is_mixed() {
886 return other.clone();
887 }
888 if other.is_mixed() {
889 return self.clone();
890 }
891 let mut result = Type::empty();
899 for a in &self.types {
900 for b in &other.types {
901 if a == b {
902 result.add_type(a.clone());
903 } else if atomic_subtype(b, a) {
904 result.add_type(b.clone());
905 } else if atomic_subtype(a, b) {
906 result.add_type(a.clone());
907 }
908 }
909 }
910 if result.is_empty() {
911 Type::never()
912 } else {
913 result
914 }
915 }
916
917 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
921 if bindings.is_empty() {
922 return self.clone();
923 }
924 if !self.types.iter().any(atomic_may_contain_templates) {
927 return self.clone();
928 }
929 let mut result = Type::empty();
930 result.possibly_undefined = self.possibly_undefined;
931 result.from_docblock = self.from_docblock;
932 for atomic in &self.types {
933 match atomic {
934 Atomic::TTemplateParam { name, .. } => {
935 if let Some(resolved) = bindings.get(name) {
936 for t in &resolved.types {
937 result.add_type(t.clone());
938 }
939 } else {
940 result.add_type(atomic.clone());
941 }
942 }
943 Atomic::TArray { key, value } => {
944 result.add_type(Atomic::TArray {
945 key: Box::new(key.substitute_templates(bindings)),
946 value: Box::new(value.substitute_templates(bindings)),
947 });
948 }
949 Atomic::TList { value } => {
950 result.add_type(Atomic::TList {
951 value: Box::new(value.substitute_templates(bindings)),
952 });
953 }
954 Atomic::TNonEmptyArray { key, value } => {
955 result.add_type(Atomic::TNonEmptyArray {
956 key: Box::new(key.substitute_templates(bindings)),
957 value: Box::new(value.substitute_templates(bindings)),
958 });
959 }
960 Atomic::TNonEmptyList { value } => {
961 result.add_type(Atomic::TNonEmptyList {
962 value: Box::new(value.substitute_templates(bindings)),
963 });
964 }
965 Atomic::TKeyedArray {
966 properties,
967 is_open,
968 is_list,
969 } => {
970 use crate::atomic::KeyedProperty;
971 let new_props = properties
972 .iter()
973 .map(|(k, prop)| {
974 (
975 k.clone(),
976 KeyedProperty {
977 ty: prop.ty.substitute_templates(bindings),
978 optional: prop.optional,
979 },
980 )
981 })
982 .collect();
983 result.add_type(Atomic::TKeyedArray {
984 properties: Box::new(new_props),
985 is_open: *is_open,
986 is_list: *is_list,
987 });
988 }
989 Atomic::TCallable {
990 params,
991 return_type,
992 } => {
993 result.add_type(Atomic::TCallable {
994 params: params.as_ref().map(|ps| {
995 ps.iter()
996 .map(|p| substitute_in_fn_param(p, bindings))
997 .collect()
998 }),
999 return_type: return_type
1000 .as_ref()
1001 .map(|r| Box::new(r.substitute_templates(bindings))),
1002 });
1003 }
1004 Atomic::TClosure { data } => {
1005 result.add_type(Atomic::TClosure {
1006 data: Box::new(crate::atomic::ClosureData {
1007 params: data
1008 .params
1009 .iter()
1010 .map(|p| substitute_in_fn_param(p, bindings))
1011 .collect(),
1012 return_type: data.return_type.substitute_templates(bindings),
1013 this_type: data
1014 .this_type
1015 .as_ref()
1016 .map(|t| t.substitute_templates(bindings)),
1017 }),
1018 });
1019 }
1020 Atomic::TConditional { data } => {
1021 let param_name = &data.param_name;
1022 let new_subject = data.subject.substitute_templates(bindings);
1023 let new_if_true = data.if_true.substitute_templates(bindings);
1024 let new_if_false = data.if_false.substitute_templates(bindings);
1025
1026 let resolved = if let Some(name) = param_name {
1030 if let Some(bound) = bindings.get(name) {
1031 if new_subject.types.len() == 1 {
1032 resolve_conditional_branch(
1033 &new_subject.types[0],
1034 bound,
1035 &new_if_true,
1036 &new_if_false,
1037 )
1038 } else {
1039 None
1040 }
1041 } else {
1042 None
1043 }
1044 } else {
1045 None
1046 };
1047
1048 if let Some(branch) = resolved {
1049 for t in branch.types {
1050 result.add_type(t);
1051 }
1052 } else {
1053 result.add_type(Atomic::TConditional {
1054 data: Box::new(crate::atomic::ConditionalData {
1055 param_name: *param_name,
1056 subject: new_subject,
1057 if_true: new_if_true,
1058 if_false: new_if_false,
1059 }),
1060 });
1061 }
1062 }
1063 Atomic::TIntersection { parts } => {
1064 result.add_type(Atomic::TIntersection {
1065 parts: vec_to_type_params(
1066 parts
1067 .iter()
1068 .map(|p| p.substitute_templates(bindings))
1069 .collect(),
1070 ),
1071 });
1072 }
1073 Atomic::TNamedObject { fqcn, type_params } => {
1074 if type_params.is_empty() && !fqcn.contains('\\') {
1081 if let Some(resolved) = bindings.get(fqcn) {
1082 for t in &resolved.types {
1083 result.add_type(t.clone());
1084 }
1085 continue;
1086 }
1087 }
1088 let new_params: Vec<Type> = type_params
1089 .iter()
1090 .map(|p| p.substitute_templates(bindings))
1091 .collect();
1092 result.add_type(Atomic::TNamedObject {
1093 fqcn: *fqcn,
1094 type_params: vec_to_type_params(new_params),
1095 });
1096 }
1097 Atomic::TClassString(Some(param_name)) => {
1099 if let Some(resolved) = bindings.get(param_name) {
1100 for r_atomic in &resolved.types {
1101 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
1102 Some(*fqcn)
1103 } else {
1104 None
1105 };
1106 result.add_type(Atomic::TClassString(cls_name));
1107 }
1108 } else {
1109 result.add_type(atomic.clone());
1110 }
1111 }
1112 Atomic::TInterfaceString(Some(param_name)) => {
1114 if let Some(resolved) = bindings.get(param_name) {
1115 for r_atomic in &resolved.types {
1116 let iface_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
1117 Some(*fqcn)
1118 } else {
1119 None
1120 };
1121 result.add_type(Atomic::TInterfaceString(iface_name));
1122 }
1123 } else {
1124 result.add_type(atomic.clone());
1125 }
1126 }
1127 _ => {
1128 result.add_type(atomic.clone());
1129 }
1130 }
1131 }
1132 result
1133 }
1134
1135 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
1141 where
1142 F: Fn(&str) -> Option<Type>,
1143 {
1144 self.resolve_conditional_inner(&lookup)
1145 }
1146
1147 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
1148 where
1149 F: Fn(&str) -> Option<Type>,
1150 {
1151 let mut result = Type::empty();
1152 for atomic in self.types {
1153 match atomic {
1154 Atomic::TConditional { ref data } => {
1155 let (param_name, subject, if_true, if_false) = (
1156 &data.param_name,
1157 &data.subject,
1158 &data.if_true,
1159 &data.if_false,
1160 );
1161 let resolved = if subject.types.len() == 1 {
1162 if let Some(name) = param_name {
1163 if let Some(arg_ty) = lookup(name.as_ref()) {
1164 resolve_conditional_branch(
1165 &subject.types[0],
1166 &arg_ty,
1167 if_true,
1168 if_false,
1169 )
1170 } else {
1171 None
1172 }
1173 } else {
1174 None
1175 }
1176 } else {
1177 None
1178 };
1179
1180 if let Some(branch) = resolved {
1181 for t in branch.resolve_conditional_inner(lookup).types {
1183 result.add_type(t);
1184 }
1185 } else {
1186 for t in if_true.clone().resolve_conditional_inner(lookup).types {
1189 result.add_type(t);
1190 }
1191 for t in if_false.clone().resolve_conditional_inner(lookup).types {
1192 result.add_type(t);
1193 }
1194 }
1195 }
1196 other => result.add_type(other),
1197 }
1198 }
1199 result
1200 }
1201
1202 pub fn is_subtype_structural(&self, other: &Type) -> bool {
1212 if other.is_mixed() {
1213 return true;
1214 }
1215 if self.is_never() {
1216 return true; }
1218 self.types
1219 .iter()
1220 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
1221 }
1222
1223 pub fn accepts_atomic_structural(&self, sub: &Atomic) -> bool {
1227 if self.is_mixed() {
1228 return true;
1229 }
1230 matches!(sub, Atomic::TNever) || self.types.iter().any(|b| atomic_subtype(sub, b))
1231 }
1232
1233 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
1236 let mut result = Type::empty();
1237 result.possibly_undefined = self.possibly_undefined;
1238 result.from_docblock = self.from_docblock;
1239 for atomic in &self.types {
1240 if f(atomic) {
1241 result.types.push(atomic.clone());
1242 }
1243 }
1244 result
1245 }
1246
1247 fn filter_replacing<K: Fn(&Atomic) -> bool, P: Fn(&Atomic) -> bool>(
1252 &self,
1253 keep: K,
1254 placeholder: P,
1255 replacement: Atomic,
1256 ) -> Type {
1257 let mut result = Type::empty();
1258 result.possibly_undefined = self.possibly_undefined;
1259 result.from_docblock = self.from_docblock;
1260 for atomic in &self.types {
1261 if keep(atomic) {
1262 result.add_type(atomic.clone());
1263 } else if placeholder(atomic) {
1264 result.add_type(replacement.clone());
1265 }
1266 }
1267 result
1268 }
1269
1270 pub fn possibly_undefined(mut self) -> Self {
1272 self.possibly_undefined = true;
1273 self
1274 }
1275
1276 pub fn from_docblock(mut self) -> Self {
1278 self.from_docblock = true;
1279 self
1280 }
1281}
1282
1283fn is_string_atomic(a: &Atomic) -> bool {
1288 matches!(
1289 a,
1290 Atomic::TString
1291 | Atomic::TNonEmptyString
1292 | Atomic::TLiteralString(_)
1293 | Atomic::TNumericString
1294 | Atomic::TClassString(_)
1295 | Atomic::TInterfaceString(_)
1296 | Atomic::TCallableString
1297 )
1298}
1299
1300fn is_array_atomic(a: &Atomic) -> bool {
1301 matches!(
1302 a,
1303 Atomic::TArray { .. }
1304 | Atomic::TNonEmptyArray { .. }
1305 | Atomic::TKeyedArray { .. }
1306 | Atomic::TList { .. }
1307 | Atomic::TNonEmptyList { .. }
1308 )
1309}
1310
1311fn is_list_atomic(a: &Atomic) -> bool {
1312 match a {
1313 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
1314 Atomic::TKeyedArray { is_list, .. } => *is_list,
1315 _ => false,
1316 }
1317}
1318
1319fn is_float_atomic(a: &Atomic) -> bool {
1320 matches!(
1321 a,
1322 Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
1323 )
1324}
1325
1326fn is_bool_atomic(a: &Atomic) -> bool {
1327 matches!(a, Atomic::TBool | Atomic::TTrue | Atomic::TFalse)
1328}
1329
1330fn resolve_conditional_branch(
1336 subject: &Atomic,
1337 arg_ty: &Type,
1338 if_true: &Type,
1339 if_false: &Type,
1340) -> Option<Type> {
1341 let predicate: fn(&Atomic) -> bool = match subject {
1342 Atomic::TNull => |a| matches!(a, Atomic::TNull),
1343 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
1344 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
1345 Atomic::TString => is_string_atomic,
1346 Atomic::TList { .. } => is_list_atomic,
1347 Atomic::TArray { .. } => is_array_atomic,
1348 Atomic::TInt => Atomic::is_int,
1349 Atomic::TFloat => is_float_atomic,
1350 Atomic::TBool => is_bool_atomic,
1351 _ => return None,
1352 };
1353
1354 if arg_ty.types.is_empty() {
1355 return None;
1356 }
1357 let all_match = arg_ty.types.iter().all(&predicate);
1358 let none_match = !arg_ty.types.iter().any(predicate);
1359 if all_match {
1360 Some(if_true.clone())
1361 } else if none_match {
1362 Some(if_false.clone())
1363 } else {
1364 None
1365 }
1366}
1367
1368fn atomic_may_contain_templates(atomic: &Atomic) -> bool {
1376 match atomic {
1377 Atomic::TNamedObject { fqcn, type_params } => {
1381 !type_params.is_empty() || !fqcn.contains('\\')
1382 }
1383 Atomic::TTemplateParam { .. }
1384 | Atomic::TArray { .. }
1385 | Atomic::TList { .. }
1386 | Atomic::TNonEmptyArray { .. }
1387 | Atomic::TNonEmptyList { .. }
1388 | Atomic::TKeyedArray { .. }
1389 | Atomic::TCallable { .. }
1390 | Atomic::TClosure { .. }
1391 | Atomic::TConditional { .. }
1392 | Atomic::TIntersection { .. }
1393 | Atomic::TClassString(Some(_))
1394 | Atomic::TInterfaceString(Some(_)) => true,
1395 _ => false,
1396 }
1397}
1398
1399fn substitute_in_fn_param(
1400 p: &crate::atomic::FnParam,
1401 bindings: &FxHashMap<Name, Type>,
1402) -> crate::atomic::FnParam {
1403 crate::atomic::FnParam {
1404 name: p.name,
1405 ty: p.ty.as_ref().map(|t| {
1406 let u = t.to_union();
1407 let substituted = u.substitute_templates(bindings);
1408 crate::compact::SimpleType::from_union(substituted)
1409 }),
1410 out_ty: p.out_ty.as_ref().map(|t| {
1411 let u = t.to_union();
1412 let substituted = u.substitute_templates(bindings);
1413 crate::compact::SimpleType::from_union(substituted)
1414 }),
1415 default: p.default.as_ref().map(|d| {
1416 let u = d.to_union();
1417 let substituted = u.substitute_templates(bindings);
1418 crate::compact::SimpleType::from_union(substituted)
1419 }),
1420 is_variadic: p.is_variadic,
1421 is_byref: p.is_byref,
1422 is_optional: p.is_optional,
1423 }
1424}
1425
1426pub fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1432 if sub == sup {
1433 return true;
1434 }
1435 match (sub, sup) {
1436 (Atomic::TNever, _) => true,
1438 (_, Atomic::TMixed) => true,
1440 (Atomic::TMixed, _) => true,
1441 (_, Atomic::TTemplateParam { as_type, .. }) => {
1446 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1447 }
1448
1449 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1451 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1452 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1453 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1454 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1455 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1456 (Atomic::TPositiveInt, Atomic::TInt) => true,
1457 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1458 (Atomic::TPositiveInt, Atomic::TNumeric) => true,
1459 (Atomic::TPositiveInt, Atomic::TScalar) => true,
1460 (Atomic::TNegativeInt, Atomic::TInt) => true,
1461 (Atomic::TNegativeInt, Atomic::TNumeric) => true,
1462 (Atomic::TNegativeInt, Atomic::TScalar) => true,
1463 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1464 (Atomic::TNonNegativeInt, Atomic::TNumeric) => true,
1465 (Atomic::TNonNegativeInt, Atomic::TScalar) => true,
1466 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1467 (Atomic::TIntRange { .. }, Atomic::TNumeric) => true,
1468 (Atomic::TIntRange { .. }, Atomic::TScalar) => true,
1469 (Atomic::TPositiveInt, Atomic::TIntRange { min, max }) => {
1471 max.is_none() && min.is_none_or(|m| m <= 1)
1472 }
1473 (Atomic::TNegativeInt, Atomic::TIntRange { min, max }) => {
1475 min.is_none() && max.is_none_or(|m| m >= -1)
1476 }
1477 (Atomic::TNonNegativeInt, Atomic::TIntRange { min, max }) => {
1479 max.is_none() && min.is_none_or(|m| m <= 0)
1480 }
1481 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TPositiveInt) => {
1483 sub_min.is_some_and(|lo| lo >= 1)
1484 }
1485 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TNonNegativeInt) => {
1486 sub_min.is_some_and(|lo| lo >= 0)
1487 }
1488 (Atomic::TIntRange { max: sub_max, .. }, Atomic::TNegativeInt) => {
1489 sub_max.is_some_and(|hi| hi <= -1)
1490 }
1491 (
1493 Atomic::TIntRange {
1494 min: sub_min,
1495 max: sub_max,
1496 },
1497 Atomic::TIntRange {
1498 min: sup_min,
1499 max: sup_max,
1500 },
1501 ) => {
1502 let lower_ok = match (sub_min, sup_min) {
1503 (_, None) => true,
1504 (None, Some(_)) => false,
1505 (Some(sl), Some(su)) => sl >= su,
1506 };
1507 let upper_ok = match (sub_max, sup_max) {
1508 (None, None) | (Some(_), None) => true,
1509 (None, Some(_)) => false,
1510 (Some(sl), Some(su)) => sl <= su,
1511 };
1512 lower_ok && upper_ok
1513 }
1514
1515 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1516 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1517 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1518
1519 (Atomic::TLiteralString(s), Atomic::TString) => {
1520 let _ = s;
1521 true
1522 }
1523 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1524 let _ = s;
1525 true
1526 }
1527 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1528 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1529 (Atomic::TLiteralString(_), Atomic::TClassString(_)) => true,
1532 (Atomic::TLiteralString(_), Atomic::TInterfaceString(_)) => true,
1535 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1536 (Atomic::TNonEmptyString, Atomic::TString) => true,
1537 (Atomic::TCallableString, Atomic::TString) => true,
1538 (Atomic::TNumericString, Atomic::TNonEmptyString) => true,
1540 (Atomic::TNumericString, Atomic::TString) => true,
1541 (Atomic::TClassString(_), Atomic::TString) => true,
1542 (Atomic::TInterfaceString(_), Atomic::TString) => true,
1543 (Atomic::TInterfaceString(_), Atomic::TClassString(None)) => true,
1548 (Atomic::TInterfaceString(Some(a)), Atomic::TClassString(Some(b))) => a == b,
1549 (Atomic::TEnumString, Atomic::TString) => true,
1550 (Atomic::TTraitString, Atomic::TString) => true,
1551
1552 (Atomic::TTrue, Atomic::TBool) => true,
1553 (Atomic::TFalse, Atomic::TBool) => true,
1554
1555 (Atomic::TInt, Atomic::TNumeric) => true,
1556 (Atomic::TFloat, Atomic::TNumeric) => true,
1557 (Atomic::TIntegralFloat, Atomic::TNumeric) => true,
1558 (Atomic::TNumericString, Atomic::TNumeric) => true,
1559
1560 (Atomic::TInt, Atomic::TScalar) => true,
1561 (Atomic::TFloat, Atomic::TScalar) => true,
1562 (Atomic::TIntegralFloat, Atomic::TScalar) => true,
1563 (Atomic::TString, Atomic::TScalar) => true,
1564 (Atomic::TBool, Atomic::TScalar) => true,
1565 (Atomic::TNumeric, Atomic::TScalar) => true,
1566 (Atomic::TTrue, Atomic::TScalar) => true,
1567 (Atomic::TFalse, Atomic::TScalar) => true,
1568
1569 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1571 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1572 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1573 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1575 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1576 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1578 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1579 (
1583 Atomic::TNamedObject {
1584 fqcn: sub_fqcn,
1585 type_params: sub_params,
1586 },
1587 Atomic::TNamedObject {
1588 fqcn: sup_fqcn,
1589 type_params: sup_params,
1590 },
1591 ) => {
1592 sub_fqcn == sup_fqcn
1593 && (sup_params.is_empty() || type_params_compatible(sub_params, sup_params))
1594 }
1595
1596 (Atomic::TIntegralFloat, Atomic::TFloat) => true,
1598
1599 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1601 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1602 (Atomic::TNegativeInt, Atomic::TFloat) => true,
1603 (Atomic::TNonNegativeInt, Atomic::TFloat) => true,
1604 (Atomic::TInt, Atomic::TFloat) => true,
1605 (Atomic::TIntRange { .. }, Atomic::TFloat) => true,
1606
1607 (Atomic::TLiteralInt(n), Atomic::TIntRange { min, max }) => {
1609 min.is_none_or(|lo| *n >= lo) && max.is_none_or(|hi| *n <= hi)
1610 }
1611
1612 (Atomic::TString, Atomic::TCallable { .. }) => true,
1614 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1615 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1616 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1617 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1618 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1619
1620 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1622 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1624 (Atomic::TClosure { data: sub }, Atomic::TClosure { data: sup }) => {
1634 fn has_nominal_type(t: &Type) -> bool {
1635 t.types.iter().any(|a| {
1636 matches!(
1637 a,
1638 Atomic::TNamedObject { .. }
1639 | Atomic::TSelf { .. }
1640 | Atomic::TStaticObject { .. }
1641 | Atomic::TTemplateParam { .. }
1642 | Atomic::TClosure { .. }
1643 | Atomic::TCallable { .. }
1644 )
1645 })
1646 }
1647 let sub_required = sub
1648 .params
1649 .iter()
1650 .filter(|p| !p.is_optional && !p.is_variadic)
1651 .count();
1652 if sub_required > sup.params.len() {
1653 false
1654 } else {
1655 let params_ok = sup.params.iter().enumerate().all(|(i, sup_param)| {
1656 let Some(sub_param) = sub.params.get(i) else {
1657 return true;
1658 };
1659 if sub_param.is_optional || sub_param.is_variadic {
1660 return true;
1661 }
1662 let (Some(sub_ty), Some(sup_ty)) =
1663 (sub_param.ty.as_ref(), sup_param.ty.as_ref())
1664 else {
1665 return true;
1666 };
1667 let (sub_u, sup_u) = (sub_ty.to_union(), sup_ty.to_union());
1668 if has_nominal_type(&sub_u) || has_nominal_type(&sup_u) {
1669 return true;
1670 }
1671 sup_u.is_subtype_structural(&sub_u)
1674 });
1675 params_ok
1676 && (sub.return_type.is_mixed()
1677 || sup.return_type.is_mixed()
1678 || has_nominal_type(&sub.return_type)
1679 || has_nominal_type(&sup.return_type)
1680 || sub.return_type.is_subtype_structural(&sup.return_type))
1681 }
1682 }
1683 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1685 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1687 fqcn.as_ref().eq_ignore_ascii_case("closure")
1688 }
1689 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1690 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1692 fqcn.as_ref().eq_ignore_ascii_case("closure")
1693 }
1694 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1696 fqcn.as_ref().eq_ignore_ascii_case("closure")
1697 }
1698
1699 (
1707 Atomic::TIntersection { parts: sub_parts },
1708 Atomic::TIntersection { parts: sup_parts },
1709 ) => sup_parts.iter().all(|sup_part| {
1710 sub_parts
1711 .iter()
1712 .any(|sub_part| sub_part.is_subtype_structural(sup_part))
1713 }),
1714
1715 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1717 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1718 }
1719 (Atomic::TNonEmptyList { value }, Atomic::TArray { key, value: av }) => {
1720 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1721 }
1722 (Atomic::TNonEmptyList { value }, Atomic::TNonEmptyArray { key, value: av }) => {
1723 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1724 }
1725 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1726 value.is_subtype_structural(lv)
1727 }
1728 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1730 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1731 && av.is_subtype_structural(lv)
1732 }
1733 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1734 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1735 && av.is_subtype_structural(lv)
1736 }
1737 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1738 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1739 && av.is_subtype_structural(lv)
1740 }
1741 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1742 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1743 && av.is_subtype_structural(lv)
1744 }
1745 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1747 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1748 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1749 }
1750
1751 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1753 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1754 }
1755
1756 (
1762 Atomic::TKeyedArray {
1763 properties,
1764 is_open,
1765 ..
1766 },
1767 Atomic::TArray { key, value },
1768 ) => {
1769 if *is_open {
1770 return true;
1771 }
1772 properties.iter().all(|(prop_key, prop)| {
1773 let key_atomic = match prop_key {
1774 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1775 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1776 };
1777 if !Type::single(key_atomic).is_subtype_structural(key) {
1778 return false; }
1780 let has_named_obj = prop.ty.types.iter().any(|a| {
1782 matches!(
1783 a,
1784 Atomic::TNamedObject { .. }
1785 | Atomic::TSelf { .. }
1786 | Atomic::TStaticObject { .. }
1787 | Atomic::TClosure { .. }
1788 | Atomic::TTemplateParam { .. }
1789 )
1790 });
1791 has_named_obj || prop.ty.is_subtype_structural(value)
1792 })
1793 }
1794 (
1795 Atomic::TKeyedArray {
1796 properties,
1797 is_open,
1798 ..
1799 },
1800 Atomic::TNonEmptyArray { key, value },
1801 ) => {
1802 if *is_open {
1803 return !properties.is_empty();
1804 }
1805 properties.iter().any(|(_, p)| !p.optional)
1806 && properties.iter().all(|(prop_key, prop)| {
1807 let key_atomic = match prop_key {
1808 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1809 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1810 };
1811 if !Type::single(key_atomic).is_subtype_structural(key) {
1812 return false;
1813 }
1814 let has_named_obj = prop.ty.types.iter().any(|a| {
1815 matches!(
1816 a,
1817 Atomic::TNamedObject { .. }
1818 | Atomic::TSelf { .. }
1819 | Atomic::TStaticObject { .. }
1820 | Atomic::TClosure { .. }
1821 | Atomic::TTemplateParam { .. }
1822 )
1823 });
1824 has_named_obj || prop.ty.is_subtype_structural(value)
1825 })
1826 }
1827
1828 (
1830 Atomic::TKeyedArray {
1831 properties,
1832 is_list,
1833 ..
1834 },
1835 Atomic::TList { value: lv },
1836 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1837 (
1838 Atomic::TKeyedArray {
1839 properties,
1840 is_list,
1841 ..
1842 },
1843 Atomic::TNonEmptyList { value: lv },
1844 ) => {
1845 *is_list
1846 && !properties.is_empty()
1847 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1848 }
1849
1850 (
1855 Atomic::TKeyedArray {
1856 properties: sub_props,
1857 is_open: sub_open,
1858 ..
1859 },
1860 Atomic::TKeyedArray {
1861 properties: sup_props,
1862 is_open: sup_open,
1863 ..
1864 },
1865 ) => {
1866 let keys_satisfied = sup_props
1867 .iter()
1868 .all(|(key, sup_prop)| match sub_props.get(key) {
1869 Some(sub_prop) => {
1870 let has_named_obj = sup_prop.ty.types.iter().any(|a| {
1871 matches!(
1872 a,
1873 Atomic::TNamedObject { .. }
1874 | Atomic::TSelf { .. }
1875 | Atomic::TStaticObject { .. }
1876 | Atomic::TClosure { .. }
1877 | Atomic::TTemplateParam { .. }
1878 )
1879 });
1880 has_named_obj || sub_prop.ty.is_subtype_structural(&sup_prop.ty)
1881 }
1882 None => sup_prop.optional || *sub_open,
1883 });
1884 let no_undeclared_extras =
1885 *sup_open || sub_props.keys().all(|k| sup_props.contains_key(k));
1886 keys_satisfied && no_undeclared_extras
1887 }
1888
1889 _ => false,
1890 }
1891}
1892
1893fn type_params_compatible(sub: &[Type], sup: &[Type]) -> bool {
1899 if sub.len() != sup.len() {
1900 return false;
1901 }
1902 sub.iter()
1903 .zip(sup.iter())
1904 .all(|(a, b)| a == b || (is_empty_array_literal(a) && is_array_like(b)))
1905}
1906
1907fn is_empty_array_literal(t: &Type) -> bool {
1910 !t.types.is_empty()
1911 && t.types.iter().all(
1912 |atom| matches!(atom, Atomic::TKeyedArray { properties, .. } if properties.is_empty()),
1913 )
1914}
1915
1916fn is_array_like(t: &Type) -> bool {
1918 !t.types.is_empty() && t.types.iter().all(|atom| atom.is_array())
1919}
1920
1921#[cfg(test)]
1926mod tests {
1927 use std::sync::Arc;
1928
1929 use super::*;
1930
1931 fn conditional(
1932 param_name: Option<Name>,
1933 subject: Type,
1934 if_true: Type,
1935 if_false: Type,
1936 ) -> Atomic {
1937 Atomic::TConditional {
1938 data: Box::new(crate::atomic::ConditionalData {
1939 param_name,
1940 subject,
1941 if_true,
1942 if_false,
1943 }),
1944 }
1945 }
1946
1947 #[test]
1948 fn single_is_single() {
1949 let u = Type::single(Atomic::TString);
1950 assert!(u.is_single());
1951 assert!(!u.is_nullable());
1952 }
1953
1954 #[test]
1955 fn nullable_has_null() {
1956 let u = Type::nullable(Atomic::TString);
1957 assert!(u.is_nullable());
1958 assert_eq!(u.types.len(), 2);
1959 }
1960
1961 #[test]
1962 fn add_type_deduplicates() {
1963 let mut u = Type::single(Atomic::TString);
1964 u.add_type(Atomic::TString);
1965 assert_eq!(u.types.len(), 1);
1966 }
1967
1968 #[test]
1969 fn array_key_is_int_string() {
1970 let k = Type::array_key();
1971 assert!(k.is_array_key());
1972 assert_eq!(k.types.len(), 2);
1973 }
1974
1975 #[test]
1976 fn is_array_key_false_for_plain_int() {
1977 assert!(!Type::int().is_array_key());
1978 }
1979
1980 #[test]
1981 fn is_array_key_false_for_mixed() {
1982 assert!(!Type::mixed().is_array_key());
1983 }
1984
1985 #[test]
1986 fn is_array_key_false_for_int_string_null() {
1987 let mut u = Type::array_key();
1988 u.add_type(Atomic::TNull);
1989 assert!(!u.is_array_key());
1990 }
1991
1992 #[test]
1993 fn add_type_literal_subsumed_by_base() {
1994 let mut u = Type::single(Atomic::TInt);
1995 u.add_type(Atomic::TLiteralInt(42));
1996 assert_eq!(u.types.len(), 1);
1997 assert!(matches!(u.types[0], Atomic::TInt));
1998 }
1999
2000 #[test]
2001 fn true_then_false_merges_to_bool() {
2002 let mut u = Type::single(Atomic::TTrue);
2003 u.add_type(Atomic::TFalse);
2004 assert_eq!(u.types.len(), 1);
2005 assert!(matches!(u.types[0], Atomic::TBool));
2006 }
2007
2008 #[test]
2009 fn false_then_true_merges_to_bool() {
2010 let mut u = Type::single(Atomic::TFalse);
2011 u.add_type(Atomic::TTrue);
2012 assert_eq!(u.types.len(), 1);
2013 assert!(matches!(u.types[0], Atomic::TBool));
2014 }
2015
2016 #[test]
2017 fn true_alone_stays_true() {
2018 let u = Type::single(Atomic::TTrue);
2019 assert_eq!(u.types.len(), 1);
2020 assert!(matches!(u.types[0], Atomic::TTrue));
2021 }
2022
2023 #[test]
2024 fn true_false_merge_preserves_other_union_members() {
2025 let mut u = Type::single(Atomic::TTrue);
2026 u.add_type(Atomic::TNull);
2027 u.add_type(Atomic::TFalse);
2028 assert_eq!(u.types.len(), 2);
2029 assert!(u.contains(|t| matches!(t, Atomic::TBool)));
2030 assert!(u.contains(|t| matches!(t, Atomic::TNull)));
2031 }
2032
2033 #[test]
2034 fn add_type_base_widens_literals() {
2035 let mut u = Type::single(Atomic::TLiteralInt(1));
2036 u.add_type(Atomic::TLiteralInt(2));
2037 u.add_type(Atomic::TInt);
2038 assert_eq!(u.types.len(), 1);
2039 assert!(matches!(u.types[0], Atomic::TInt));
2040 }
2041
2042 #[test]
2043 fn mixed_subsumes_everything() {
2044 let mut u = Type::single(Atomic::TString);
2045 u.add_type(Atomic::TMixed);
2046 assert_eq!(u.types.len(), 1);
2047 assert!(u.is_mixed());
2048 }
2049
2050 #[test]
2051 fn remove_null() {
2052 let u = Type::nullable(Atomic::TString);
2053 let narrowed = u.remove_null();
2054 assert!(!narrowed.is_nullable());
2055 assert_eq!(narrowed.types.len(), 1);
2056 }
2057
2058 #[test]
2059 fn narrow_to_truthy_removes_null_false() {
2060 let mut u = Type::empty();
2061 u.add_type(Atomic::TString);
2062 u.add_type(Atomic::TNull);
2063 u.add_type(Atomic::TFalse);
2064 let truthy = u.narrow_to_truthy();
2065 assert!(!truthy.is_nullable());
2066 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
2067 }
2068
2069 #[test]
2070 fn merge_combines_types() {
2071 let a = Type::single(Atomic::TString);
2072 let b = Type::single(Atomic::TInt);
2073 let merged = Type::merge(&a, &b);
2074 assert_eq!(merged.types.len(), 2);
2075 }
2076
2077 #[test]
2078 fn intersect_keeps_narrower_side_not_self() {
2079 let int_ty = Type::single(Atomic::TInt);
2083 let mut literals = Type::empty();
2084 literals.add_type(Atomic::TLiteralInt(1));
2085 literals.add_type(Atomic::TLiteralInt(2));
2086
2087 let narrowed = int_ty.intersect_with(&literals);
2088 assert_eq!(narrowed.types.len(), 2);
2089 assert!(narrowed.contains(|t| matches!(t, Atomic::TLiteralInt(1))));
2090 assert!(narrowed.contains(|t| matches!(t, Atomic::TLiteralInt(2))));
2091 assert!(!narrowed.contains(|t| matches!(t, Atomic::TInt)));
2092 }
2093
2094 #[test]
2095 fn subtype_literal_int_under_int() {
2096 let sub = Type::single(Atomic::TLiteralInt(5));
2097 let sup = Type::single(Atomic::TInt);
2098 assert!(sub.is_subtype_structural(&sup));
2099 }
2100
2101 #[test]
2102 fn subtype_never_is_bottom() {
2103 let never = Type::never();
2104 let string = Type::single(Atomic::TString);
2105 assert!(never.is_subtype_structural(&string));
2106 }
2107
2108 #[test]
2109 fn subtype_everything_under_mixed() {
2110 let string = Type::single(Atomic::TString);
2111 let mixed = Type::mixed();
2112 assert!(string.is_subtype_structural(&mixed));
2113 }
2114
2115 #[test]
2116 fn template_substitution() {
2117 let mut bindings = FxHashMap::default();
2118 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
2119
2120 let tmpl = Type::single(Atomic::TTemplateParam {
2121 name: Name::new("T"),
2122 as_type: Box::new(Type::mixed()),
2123 defining_entity: Name::new("MyClass"),
2124 });
2125
2126 let resolved = tmpl.substitute_templates(&bindings);
2127 assert_eq!(resolved.types.len(), 1);
2128 assert!(matches!(resolved.types[0], Atomic::TString));
2129 }
2130
2131 #[test]
2132 fn intersection_is_object() {
2133 let parts = vec![
2134 Type::single(Atomic::TNamedObject {
2135 fqcn: Name::new("Iterator"),
2136 type_params: empty_type_params(),
2137 }),
2138 Type::single(Atomic::TNamedObject {
2139 fqcn: Name::new("Countable"),
2140 type_params: empty_type_params(),
2141 }),
2142 ];
2143 let atomic = Atomic::TIntersection {
2144 parts: vec_to_type_params(parts),
2145 };
2146 assert!(atomic.is_object());
2147 assert!(!atomic.can_be_falsy());
2148 assert!(atomic.can_be_truthy());
2149 }
2150
2151 #[test]
2152 fn intersection_display_two_parts() {
2153 let parts = vec![
2154 Type::single(Atomic::TNamedObject {
2155 fqcn: Name::new("Iterator"),
2156 type_params: empty_type_params(),
2157 }),
2158 Type::single(Atomic::TNamedObject {
2159 fqcn: Name::new("Countable"),
2160 type_params: empty_type_params(),
2161 }),
2162 ];
2163 let u = Type::single(Atomic::TIntersection {
2164 parts: vec_to_type_params(parts),
2165 });
2166 assert_eq!(format!("{u}"), "Iterator&Countable");
2167 }
2168
2169 #[test]
2170 fn intersection_display_three_parts() {
2171 let parts = vec![
2172 Type::single(Atomic::TNamedObject {
2173 fqcn: Name::new("A"),
2174 type_params: empty_type_params(),
2175 }),
2176 Type::single(Atomic::TNamedObject {
2177 fqcn: Name::new("B"),
2178 type_params: empty_type_params(),
2179 }),
2180 Type::single(Atomic::TNamedObject {
2181 fqcn: Name::new("C"),
2182 type_params: empty_type_params(),
2183 }),
2184 ];
2185 let u = Type::single(Atomic::TIntersection {
2186 parts: vec_to_type_params(parts),
2187 });
2188 assert_eq!(format!("{u}"), "A&B&C");
2189 }
2190
2191 #[test]
2192 fn intersection_in_nullable_union_display() {
2193 let intersection = Atomic::TIntersection {
2194 parts: vec_to_type_params(vec![
2195 Type::single(Atomic::TNamedObject {
2196 fqcn: Name::new("Iterator"),
2197 type_params: empty_type_params(),
2198 }),
2199 Type::single(Atomic::TNamedObject {
2200 fqcn: Name::new("Countable"),
2201 type_params: empty_type_params(),
2202 }),
2203 ]),
2204 };
2205 let mut u = Type::single(intersection);
2206 u.add_type(Atomic::TNull);
2207 assert!(u.is_nullable());
2208 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
2209 }
2210
2211 fn t_param(name: &str) -> Type {
2214 Type::single(Atomic::TTemplateParam {
2215 name: Name::new(name),
2216 as_type: Box::new(Type::mixed()),
2217 defining_entity: Name::new("Fn"),
2218 })
2219 }
2220
2221 fn bindings_t_string() -> FxHashMap<Name, Type> {
2222 let mut b = FxHashMap::default();
2223 b.insert(Name::new("T"), Type::single(Atomic::TString));
2224 b
2225 }
2226
2227 #[test]
2228 fn substitute_non_empty_array_key_and_value() {
2229 let ty = Type::single(Atomic::TNonEmptyArray {
2230 key: Box::new(t_param("T")),
2231 value: Box::new(t_param("T")),
2232 });
2233 let result = ty.substitute_templates(&bindings_t_string());
2234 assert_eq!(result.types.len(), 1);
2235 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
2236 panic!("expected TNonEmptyArray");
2237 };
2238 assert!(matches!(key.types[0], Atomic::TString));
2239 assert!(matches!(value.types[0], Atomic::TString));
2240 }
2241
2242 #[test]
2243 fn substitute_non_empty_list_value() {
2244 let ty = Type::single(Atomic::TNonEmptyList {
2245 value: Box::new(t_param("T")),
2246 });
2247 let result = ty.substitute_templates(&bindings_t_string());
2248 let Atomic::TNonEmptyList { value } = &result.types[0] else {
2249 panic!("expected TNonEmptyList");
2250 };
2251 assert!(matches!(value.types[0], Atomic::TString));
2252 }
2253
2254 #[test]
2255 fn substitute_keyed_array_property_types() {
2256 use crate::atomic::{ArrayKey, KeyedProperty};
2257 use indexmap::IndexMap;
2258 let mut props = IndexMap::new();
2259 props.insert(
2260 ArrayKey::String(Arc::from("name")),
2261 KeyedProperty {
2262 ty: t_param("T"),
2263 optional: false,
2264 },
2265 );
2266 props.insert(
2267 ArrayKey::String(Arc::from("tag")),
2268 KeyedProperty {
2269 ty: t_param("T"),
2270 optional: true,
2271 },
2272 );
2273 let ty = Type::single(Atomic::TKeyedArray {
2274 properties: Box::new(props),
2275 is_open: true,
2276 is_list: false,
2277 });
2278 let result = ty.substitute_templates(&bindings_t_string());
2279 let Atomic::TKeyedArray {
2280 properties,
2281 is_open,
2282 is_list,
2283 } = &result.types[0]
2284 else {
2285 panic!("expected TKeyedArray");
2286 };
2287 assert!(is_open);
2288 assert!(!is_list);
2289 assert!(matches!(
2290 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
2291 Atomic::TString
2292 ));
2293 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
2294 assert!(matches!(
2295 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
2296 Atomic::TString
2297 ));
2298 }
2299
2300 #[test]
2301 fn substitute_callable_params_and_return() {
2302 use crate::atomic::FnParam;
2303 let ty = Type::single(Atomic::TCallable {
2304 params: Some(Box::new([FnParam {
2305 name: Name::new("x"),
2306 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2307 out_ty: None,
2308 default: None,
2309 is_variadic: false,
2310 is_byref: false,
2311 is_optional: false,
2312 }])),
2313 return_type: Some(Box::new(t_param("T"))),
2314 });
2315 let result = ty.substitute_templates(&bindings_t_string());
2316 let Atomic::TCallable {
2317 params,
2318 return_type,
2319 } = &result.types[0]
2320 else {
2321 panic!("expected TCallable");
2322 };
2323 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
2324 let param_union = param_ty.to_union();
2325 assert!(matches!(param_union.types[0], Atomic::TString));
2326 let ret = return_type.as_ref().unwrap();
2327 assert!(matches!(ret.types[0], Atomic::TString));
2328 }
2329
2330 #[test]
2331 fn substitute_callable_bare_no_panic() {
2332 let ty = Type::single(Atomic::TCallable {
2334 params: None,
2335 return_type: None,
2336 });
2337 let result = ty.substitute_templates(&bindings_t_string());
2338 assert!(matches!(
2339 result.types[0],
2340 Atomic::TCallable {
2341 params: None,
2342 return_type: None
2343 }
2344 ));
2345 }
2346
2347 #[test]
2348 fn substitute_closure_params_return_and_this() {
2349 use crate::atomic::FnParam;
2350 let ty = Type::single(Atomic::TClosure {
2351 data: Box::new(crate::atomic::ClosureData {
2352 params: Box::new([FnParam {
2353 name: Name::new("a"),
2354 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2355 out_ty: None,
2356 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2357 is_variadic: true,
2358 is_byref: true,
2359 is_optional: true,
2360 }]),
2361 return_type: t_param("T"),
2362 this_type: Some(t_param("T")),
2363 }),
2364 });
2365 let result = ty.substitute_templates(&bindings_t_string());
2366 let Atomic::TClosure { data } = &result.types[0] else {
2367 panic!("expected TClosure");
2368 };
2369 let (params, return_type, this_type) = (&data.params, &data.return_type, &data.this_type);
2370 let p = ¶ms[0];
2371 let ty_union = p.ty.as_ref().unwrap().to_union();
2372 let default_union = p.default.as_ref().unwrap().to_union();
2373 assert!(matches!(ty_union.types[0], Atomic::TString));
2374 assert!(matches!(default_union.types[0], Atomic::TString));
2375 assert!(p.is_variadic);
2377 assert!(p.is_byref);
2378 assert!(p.is_optional);
2379 assert!(matches!(return_type.types[0], Atomic::TString));
2380 assert!(matches!(
2381 this_type.as_ref().unwrap().types[0],
2382 Atomic::TString
2383 ));
2384 }
2385
2386 #[test]
2387 fn substitute_conditional_all_branches() {
2388 let ty = Type::single(conditional(
2389 None,
2390 t_param("T"),
2391 t_param("T"),
2392 Type::single(Atomic::TInt),
2393 ));
2394 let result = ty.substitute_templates(&bindings_t_string());
2395 let Atomic::TConditional { data } = &result.types[0] else {
2396 panic!("expected TConditional");
2397 };
2398 let (subject, if_true, if_false) = (&data.subject, &data.if_true, &data.if_false);
2399 assert!(matches!(subject.types[0], Atomic::TString));
2400 assert!(matches!(if_true.types[0], Atomic::TString));
2401 assert!(matches!(if_false.types[0], Atomic::TInt));
2402 }
2403
2404 #[test]
2405 fn resolve_conditional_is_null_non_null_arg() {
2406 let ty = Type::single(conditional(
2407 Some(Name::new("x")),
2408 Type::single(Atomic::TNull),
2409 Type::single(Atomic::TInt),
2410 Type::single(Atomic::TString),
2411 ));
2412 let result = ty.resolve_conditional_returns(|name| {
2413 if name == "x" {
2414 Some(Type::single(Atomic::TString)) } else {
2416 None
2417 }
2418 });
2419 assert!(result.types.len() == 1);
2420 assert!(matches!(result.types[0], Atomic::TString));
2421 }
2422
2423 #[test]
2424 fn resolve_conditional_is_null_null_arg() {
2425 let ty = Type::single(conditional(
2426 Some(Name::new("x")),
2427 Type::single(Atomic::TNull),
2428 Type::single(Atomic::TInt),
2429 Type::single(Atomic::TString),
2430 ));
2431 let result = ty.resolve_conditional_returns(|name| {
2432 if name == "x" {
2433 Some(Type::single(Atomic::TNull)) } else {
2435 None
2436 }
2437 });
2438 assert!(result.types.len() == 1);
2439 assert!(matches!(result.types[0], Atomic::TInt));
2440 }
2441
2442 #[test]
2443 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
2444 let mut nullable_str = Type::single(Atomic::TString);
2445 nullable_str.add_type(Atomic::TNull);
2446 let ty = Type::single(conditional(
2447 Some(Name::new("x")),
2448 Type::single(Atomic::TNull),
2449 Type::single(Atomic::TInt),
2450 Type::single(Atomic::TString),
2451 ));
2452 let result = ty.resolve_conditional_returns(|name| {
2453 if name == "x" {
2454 Some(nullable_str.clone())
2455 } else {
2456 None
2457 }
2458 });
2459 assert_eq!(result.types.len(), 2);
2461 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2462 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2463 }
2464
2465 #[test]
2466 fn resolve_conditional_nested_widens_inner_branch() {
2467 let inner = Type::single(conditional(
2470 Some(Name::new("x")),
2471 Type::single(Atomic::TString),
2472 Type::single(Atomic::TString),
2473 Type::single(Atomic::TFloat),
2474 ));
2475 let ty = Type::single(conditional(
2476 Some(Name::new("x")),
2477 Type::single(Atomic::TNull),
2478 Type::single(Atomic::TInt),
2479 inner,
2480 ));
2481 let result = ty.resolve_conditional_returns(|_| None);
2483 assert!(
2484 result
2485 .types
2486 .iter()
2487 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2488 "no TConditional should survive: {:?}",
2489 result.types
2490 );
2491 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2492 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2493 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
2494 }
2495
2496 #[test]
2497 fn resolve_conditional_nested_resolves_inner_branch() {
2498 let inner = Type::single(conditional(
2502 Some(Name::new("x")),
2503 Type::single(Atomic::TString),
2504 Type::single(Atomic::TString),
2505 Type::single(Atomic::TFloat),
2506 ));
2507 let ty = Type::single(conditional(
2508 Some(Name::new("x")),
2509 Type::single(Atomic::TNull),
2510 Type::single(Atomic::TInt),
2511 inner,
2512 ));
2513 let result = ty.resolve_conditional_returns(|name| {
2515 if name == "x" {
2516 Some(Type::single(Atomic::TString))
2517 } else {
2518 None
2519 }
2520 });
2521 assert!(
2522 result
2523 .types
2524 .iter()
2525 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2526 "no TConditional should survive: {:?}",
2527 result.types
2528 );
2529 assert_eq!(result.types.len(), 1);
2530 assert!(matches!(result.types[0], Atomic::TString));
2531 }
2532
2533 #[test]
2534 fn substitute_intersection_parts() {
2535 let ty = Type::single(Atomic::TIntersection {
2536 parts: vec_to_type_params(vec![
2537 Type::single(Atomic::TNamedObject {
2538 fqcn: Name::new("Countable"),
2539 type_params: empty_type_params(),
2540 }),
2541 t_param("T"),
2542 ]),
2543 });
2544 let result = ty.substitute_templates(&bindings_t_string());
2545 let Atomic::TIntersection { parts } = &result.types[0] else {
2546 panic!("expected TIntersection");
2547 };
2548 assert_eq!(parts.len(), 2);
2549 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
2550 assert!(matches!(parts[1].types[0], Atomic::TString));
2551 }
2552
2553 #[test]
2554 fn substitute_no_template_params_identity() {
2555 let ty = Type::single(Atomic::TInt);
2556 let result = ty.substitute_templates(&bindings_t_string());
2557 assert!(matches!(result.types[0], Atomic::TInt));
2558 }
2559}