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 {
659 let mut out = Type::empty();
660 out.from_docblock = self.from_docblock;
661 for t in &self.types {
662 match t {
663 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => out.add_type(t.clone()),
664 Atomic::TArray { key, value } if matches!(key.types.as_slice(), [Atomic::TInt]) => {
665 out.add_type(Atomic::TList {
666 value: value.clone(),
667 });
668 }
669 Atomic::TNonEmptyArray { key, value }
670 if matches!(key.types.as_slice(), [Atomic::TInt]) =>
671 {
672 out.add_type(Atomic::TNonEmptyList {
673 value: value.clone(),
674 });
675 }
676 Atomic::TMixed => out.add_type(Atomic::TList {
677 value: Box::new(Type::mixed()),
678 }),
679 _ => {}
680 }
681 }
682 if out.is_empty() {
683 self.filter(|t| matches!(t, Atomic::TList { .. } | Atomic::TNonEmptyList { .. }))
684 } else {
685 out
686 }
687 }
688
689 pub fn narrow_to_object(&self) -> Type {
694 let mut out = Type::empty();
695 for t in &self.types {
696 if matches!(t, Atomic::TMixed) {
697 out.add_type(Atomic::TObject);
698 } else if t.is_object() || matches!(t, Atomic::TTemplateParam { .. }) {
699 out.add_type(t.clone());
700 }
701 }
702 if out.types.is_empty() {
703 self.filter(|t| t.is_object())
704 } else {
705 out
706 }
707 }
708
709 pub fn narrow_to_callable(&self) -> Type {
716 self.filter(|t| {
717 t.is_callable()
718 || t.is_string()
719 || t.is_array()
720 || t.is_object()
721 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
722 })
723 }
724
725 pub fn narrow_to_scalar(&self) -> Type {
727 self.filter_replacing(
728 |t| {
729 t.is_string()
730 || t.is_int()
731 || matches!(
732 t,
733 Atomic::TFloat
734 | Atomic::TIntegralFloat
735 | Atomic::TLiteralFloat(..)
736 | Atomic::TBool
737 | Atomic::TTrue
738 | Atomic::TFalse
739 | Atomic::TScalar
740 | Atomic::TNumeric
741 | Atomic::TNumericString
742 | Atomic::TTemplateParam { .. }
743 )
744 },
745 |t| matches!(t, Atomic::TMixed),
746 Atomic::TScalar,
747 )
748 }
749
750 pub fn narrow_to_iterable(&self) -> Type {
753 self.filter(|t| {
754 t.is_array()
755 || t.is_object()
756 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
757 })
758 }
759
760 pub fn narrow_to_countable(&self) -> Type {
763 self.filter(|t| {
764 t.is_array()
765 || t.is_object()
766 || matches!(t, Atomic::TMixed | Atomic::TTemplateParam { .. })
767 })
768 }
769
770 pub fn narrow_to_resource(&self) -> Type {
774 self.filter(|t| matches!(t, Atomic::TMixed))
776 }
777
778 pub fn narrow_to_class_string(&self) -> Type {
783 let mut out = Type::empty();
784 out.from_docblock = self.from_docblock;
785 for t in &self.types {
786 match t {
787 Atomic::TClassString(_) => out.add_type(t.clone()),
788 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
789 out.add_type(Atomic::TClassString(None));
790 }
791 _ => {}
792 }
793 }
794 out
795 }
796
797 pub fn narrow_to_interface_string(&self) -> Type {
802 let mut out = Type::empty();
803 out.from_docblock = self.from_docblock;
804 for t in &self.types {
805 match t {
806 Atomic::TInterfaceString(_) => out.add_type(t.clone()),
807 Atomic::TClassString(name) => {
811 out.add_type(Atomic::TInterfaceString(*name));
812 }
813 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
814 out.add_type(Atomic::TInterfaceString(None));
815 }
816 _ => {}
817 }
818 }
819 out
820 }
821
822 pub fn merge(a: &Type, b: &Type) -> Type {
827 if b.types.is_empty() {
829 let mut result = a.clone();
830 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
831 return result;
832 }
833 if a.types.is_empty() {
835 let mut result = b.clone();
836 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
837 return result;
838 }
839 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
841 let mut result = a.clone();
842 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
843 return result;
844 }
845 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
847 return Type {
848 types: smallvec::smallvec![Atomic::TMixed],
849 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
850 from_docblock: a.from_docblock || b.from_docblock,
851 };
852 }
853 let mut result = a.clone();
854 result.merge_with(b);
855 result
856 }
857
858 pub fn merge_with(&mut self, other: &Type) {
860 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
861 self.possibly_undefined |= other.possibly_undefined;
862 return;
863 }
864 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
865 self.types.clear();
866 self.types.push(Atomic::TMixed);
867 self.possibly_undefined |= other.possibly_undefined;
868 return;
869 }
870 for atomic in &other.types {
871 self.add_type(atomic.clone());
872 }
873 self.possibly_undefined |= other.possibly_undefined;
874 }
875
876 pub fn intersect_with(&self, other: &Type) -> Type {
880 if self.is_mixed() {
881 return other.clone();
882 }
883 if other.is_mixed() {
884 return self.clone();
885 }
886 let mut result = Type::empty();
894 for a in &self.types {
895 for b in &other.types {
896 if a == b {
897 result.add_type(a.clone());
898 } else if atomic_subtype(b, a) {
899 result.add_type(b.clone());
900 } else if atomic_subtype(a, b) {
901 result.add_type(a.clone());
902 }
903 }
904 }
905 if result.is_empty() {
906 Type::never()
907 } else {
908 result
909 }
910 }
911
912 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
916 if bindings.is_empty() {
917 return self.clone();
918 }
919 if !self.types.iter().any(atomic_may_contain_templates) {
922 return self.clone();
923 }
924 let mut result = Type::empty();
925 result.possibly_undefined = self.possibly_undefined;
926 result.from_docblock = self.from_docblock;
927 for atomic in &self.types {
928 match atomic {
929 Atomic::TTemplateParam { name, .. } => {
930 if let Some(resolved) = bindings.get(name) {
931 for t in &resolved.types {
932 result.add_type(t.clone());
933 }
934 } else {
935 result.add_type(atomic.clone());
936 }
937 }
938 Atomic::TArray { key, value } => {
939 result.add_type(Atomic::TArray {
940 key: Box::new(key.substitute_templates(bindings)),
941 value: Box::new(value.substitute_templates(bindings)),
942 });
943 }
944 Atomic::TList { value } => {
945 result.add_type(Atomic::TList {
946 value: Box::new(value.substitute_templates(bindings)),
947 });
948 }
949 Atomic::TNonEmptyArray { key, value } => {
950 result.add_type(Atomic::TNonEmptyArray {
951 key: Box::new(key.substitute_templates(bindings)),
952 value: Box::new(value.substitute_templates(bindings)),
953 });
954 }
955 Atomic::TNonEmptyList { value } => {
956 result.add_type(Atomic::TNonEmptyList {
957 value: Box::new(value.substitute_templates(bindings)),
958 });
959 }
960 Atomic::TKeyedArray {
961 properties,
962 is_open,
963 is_list,
964 } => {
965 use crate::atomic::KeyedProperty;
966 let new_props = properties
967 .iter()
968 .map(|(k, prop)| {
969 (
970 k.clone(),
971 KeyedProperty {
972 ty: prop.ty.substitute_templates(bindings),
973 optional: prop.optional,
974 },
975 )
976 })
977 .collect();
978 result.add_type(Atomic::TKeyedArray {
979 properties: Box::new(new_props),
980 is_open: *is_open,
981 is_list: *is_list,
982 });
983 }
984 Atomic::TCallable {
985 params,
986 return_type,
987 } => {
988 result.add_type(Atomic::TCallable {
989 params: params.as_ref().map(|ps| {
990 ps.iter()
991 .map(|p| substitute_in_fn_param(p, bindings))
992 .collect()
993 }),
994 return_type: return_type
995 .as_ref()
996 .map(|r| Box::new(r.substitute_templates(bindings))),
997 });
998 }
999 Atomic::TClosure { data } => {
1000 result.add_type(Atomic::TClosure {
1001 data: Box::new(crate::atomic::ClosureData {
1002 params: data
1003 .params
1004 .iter()
1005 .map(|p| substitute_in_fn_param(p, bindings))
1006 .collect(),
1007 return_type: data.return_type.substitute_templates(bindings),
1008 this_type: data
1009 .this_type
1010 .as_ref()
1011 .map(|t| t.substitute_templates(bindings)),
1012 }),
1013 });
1014 }
1015 Atomic::TConditional { data } => {
1016 let param_name = &data.param_name;
1017 let new_subject = data.subject.substitute_templates(bindings);
1018 let new_if_true = data.if_true.substitute_templates(bindings);
1019 let new_if_false = data.if_false.substitute_templates(bindings);
1020
1021 let resolved = if let Some(name) = param_name {
1025 if let Some(bound) = bindings.get(name) {
1026 if new_subject.types.len() == 1 {
1027 resolve_conditional_branch(
1028 &new_subject.types[0],
1029 bound,
1030 &new_if_true,
1031 &new_if_false,
1032 )
1033 } else {
1034 None
1035 }
1036 } else {
1037 None
1038 }
1039 } else {
1040 None
1041 };
1042
1043 if let Some(branch) = resolved {
1044 for t in branch.types {
1045 result.add_type(t);
1046 }
1047 } else {
1048 result.add_type(Atomic::TConditional {
1049 data: Box::new(crate::atomic::ConditionalData {
1050 param_name: *param_name,
1051 subject: new_subject,
1052 if_true: new_if_true,
1053 if_false: new_if_false,
1054 }),
1055 });
1056 }
1057 }
1058 Atomic::TIntersection { parts } => {
1059 result.add_type(Atomic::TIntersection {
1060 parts: vec_to_type_params(
1061 parts
1062 .iter()
1063 .map(|p| p.substitute_templates(bindings))
1064 .collect(),
1065 ),
1066 });
1067 }
1068 Atomic::TNamedObject { fqcn, type_params } => {
1069 if type_params.is_empty() && !fqcn.contains('\\') {
1076 if let Some(resolved) = bindings.get(fqcn) {
1077 for t in &resolved.types {
1078 result.add_type(t.clone());
1079 }
1080 continue;
1081 }
1082 }
1083 let new_params: Vec<Type> = type_params
1084 .iter()
1085 .map(|p| p.substitute_templates(bindings))
1086 .collect();
1087 result.add_type(Atomic::TNamedObject {
1088 fqcn: *fqcn,
1089 type_params: vec_to_type_params(new_params),
1090 });
1091 }
1092 Atomic::TClassString(Some(param_name)) => {
1094 if let Some(resolved) = bindings.get(param_name) {
1095 for r_atomic in &resolved.types {
1096 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
1097 Some(*fqcn)
1098 } else {
1099 None
1100 };
1101 result.add_type(Atomic::TClassString(cls_name));
1102 }
1103 } else {
1104 result.add_type(atomic.clone());
1105 }
1106 }
1107 Atomic::TInterfaceString(Some(param_name)) => {
1109 if let Some(resolved) = bindings.get(param_name) {
1110 for r_atomic in &resolved.types {
1111 let iface_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
1112 Some(*fqcn)
1113 } else {
1114 None
1115 };
1116 result.add_type(Atomic::TInterfaceString(iface_name));
1117 }
1118 } else {
1119 result.add_type(atomic.clone());
1120 }
1121 }
1122 _ => {
1123 result.add_type(atomic.clone());
1124 }
1125 }
1126 }
1127 result
1128 }
1129
1130 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
1136 where
1137 F: Fn(&str) -> Option<Type>,
1138 {
1139 self.resolve_conditional_inner(&lookup)
1140 }
1141
1142 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
1143 where
1144 F: Fn(&str) -> Option<Type>,
1145 {
1146 let mut result = Type::empty();
1147 for atomic in self.types {
1148 match atomic {
1149 Atomic::TConditional { ref data } => {
1150 let (param_name, subject, if_true, if_false) = (
1151 &data.param_name,
1152 &data.subject,
1153 &data.if_true,
1154 &data.if_false,
1155 );
1156 let resolved = if subject.types.len() == 1 {
1157 if let Some(name) = param_name {
1158 if let Some(arg_ty) = lookup(name.as_ref()) {
1159 resolve_conditional_branch(
1160 &subject.types[0],
1161 &arg_ty,
1162 if_true,
1163 if_false,
1164 )
1165 } else {
1166 None
1167 }
1168 } else {
1169 None
1170 }
1171 } else {
1172 None
1173 };
1174
1175 if let Some(branch) = resolved {
1176 for t in branch.resolve_conditional_inner(lookup).types {
1178 result.add_type(t);
1179 }
1180 } else {
1181 for t in if_true.clone().resolve_conditional_inner(lookup).types {
1184 result.add_type(t);
1185 }
1186 for t in if_false.clone().resolve_conditional_inner(lookup).types {
1187 result.add_type(t);
1188 }
1189 }
1190 }
1191 other => result.add_type(other),
1192 }
1193 }
1194 result
1195 }
1196
1197 pub fn is_subtype_structural(&self, other: &Type) -> bool {
1207 if other.is_mixed() {
1208 return true;
1209 }
1210 if self.is_never() {
1211 return true; }
1213 self.types
1214 .iter()
1215 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
1216 }
1217
1218 pub fn accepts_atomic_structural(&self, sub: &Atomic) -> bool {
1222 if self.is_mixed() {
1223 return true;
1224 }
1225 matches!(sub, Atomic::TNever) || self.types.iter().any(|b| atomic_subtype(sub, b))
1226 }
1227
1228 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
1231 let mut result = Type::empty();
1232 result.possibly_undefined = self.possibly_undefined;
1233 result.from_docblock = self.from_docblock;
1234 for atomic in &self.types {
1235 if f(atomic) {
1236 result.types.push(atomic.clone());
1237 }
1238 }
1239 result
1240 }
1241
1242 fn filter_replacing<K: Fn(&Atomic) -> bool, P: Fn(&Atomic) -> bool>(
1247 &self,
1248 keep: K,
1249 placeholder: P,
1250 replacement: Atomic,
1251 ) -> Type {
1252 let mut result = Type::empty();
1253 result.possibly_undefined = self.possibly_undefined;
1254 result.from_docblock = self.from_docblock;
1255 for atomic in &self.types {
1256 if keep(atomic) {
1257 result.add_type(atomic.clone());
1258 } else if placeholder(atomic) {
1259 result.add_type(replacement.clone());
1260 }
1261 }
1262 result
1263 }
1264
1265 pub fn possibly_undefined(mut self) -> Self {
1267 self.possibly_undefined = true;
1268 self
1269 }
1270
1271 pub fn from_docblock(mut self) -> Self {
1273 self.from_docblock = true;
1274 self
1275 }
1276}
1277
1278fn is_string_atomic(a: &Atomic) -> bool {
1283 matches!(
1284 a,
1285 Atomic::TString
1286 | Atomic::TNonEmptyString
1287 | Atomic::TLiteralString(_)
1288 | Atomic::TNumericString
1289 | Atomic::TClassString(_)
1290 | Atomic::TInterfaceString(_)
1291 | Atomic::TCallableString
1292 )
1293}
1294
1295fn is_array_atomic(a: &Atomic) -> bool {
1296 matches!(
1297 a,
1298 Atomic::TArray { .. }
1299 | Atomic::TNonEmptyArray { .. }
1300 | Atomic::TKeyedArray { .. }
1301 | Atomic::TList { .. }
1302 | Atomic::TNonEmptyList { .. }
1303 )
1304}
1305
1306fn is_list_atomic(a: &Atomic) -> bool {
1307 match a {
1308 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
1309 Atomic::TKeyedArray { is_list, .. } => *is_list,
1310 _ => false,
1311 }
1312}
1313
1314fn is_float_atomic(a: &Atomic) -> bool {
1315 matches!(
1316 a,
1317 Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..)
1318 )
1319}
1320
1321fn is_bool_atomic(a: &Atomic) -> bool {
1322 matches!(a, Atomic::TBool | Atomic::TTrue | Atomic::TFalse)
1323}
1324
1325fn resolve_conditional_branch(
1331 subject: &Atomic,
1332 arg_ty: &Type,
1333 if_true: &Type,
1334 if_false: &Type,
1335) -> Option<Type> {
1336 let predicate: fn(&Atomic) -> bool = match subject {
1337 Atomic::TNull => |a| matches!(a, Atomic::TNull),
1338 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
1339 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
1340 Atomic::TString => is_string_atomic,
1341 Atomic::TList { .. } => is_list_atomic,
1342 Atomic::TArray { .. } => is_array_atomic,
1343 Atomic::TInt => Atomic::is_int,
1344 Atomic::TFloat => is_float_atomic,
1345 Atomic::TBool => is_bool_atomic,
1346 _ => return None,
1347 };
1348
1349 if arg_ty.types.is_empty() {
1350 return None;
1351 }
1352 let all_match = arg_ty.types.iter().all(&predicate);
1353 let none_match = !arg_ty.types.iter().any(predicate);
1354 if all_match {
1355 Some(if_true.clone())
1356 } else if none_match {
1357 Some(if_false.clone())
1358 } else {
1359 None
1360 }
1361}
1362
1363fn atomic_may_contain_templates(atomic: &Atomic) -> bool {
1371 match atomic {
1372 Atomic::TNamedObject { fqcn, type_params } => {
1376 !type_params.is_empty() || !fqcn.contains('\\')
1377 }
1378 Atomic::TTemplateParam { .. }
1379 | Atomic::TArray { .. }
1380 | Atomic::TList { .. }
1381 | Atomic::TNonEmptyArray { .. }
1382 | Atomic::TNonEmptyList { .. }
1383 | Atomic::TKeyedArray { .. }
1384 | Atomic::TCallable { .. }
1385 | Atomic::TClosure { .. }
1386 | Atomic::TConditional { .. }
1387 | Atomic::TIntersection { .. }
1388 | Atomic::TClassString(Some(_))
1389 | Atomic::TInterfaceString(Some(_)) => true,
1390 _ => false,
1391 }
1392}
1393
1394fn substitute_in_fn_param(
1395 p: &crate::atomic::FnParam,
1396 bindings: &FxHashMap<Name, Type>,
1397) -> crate::atomic::FnParam {
1398 crate::atomic::FnParam {
1399 name: p.name,
1400 ty: p.ty.as_ref().map(|t| {
1401 let u = t.to_union();
1402 let substituted = u.substitute_templates(bindings);
1403 crate::compact::SimpleType::from_union(substituted)
1404 }),
1405 out_ty: p.out_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 default: p.default.as_ref().map(|d| {
1411 let u = d.to_union();
1412 let substituted = u.substitute_templates(bindings);
1413 crate::compact::SimpleType::from_union(substituted)
1414 }),
1415 is_variadic: p.is_variadic,
1416 is_byref: p.is_byref,
1417 is_optional: p.is_optional,
1418 }
1419}
1420
1421pub fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1427 if sub == sup {
1428 return true;
1429 }
1430 match (sub, sup) {
1431 (Atomic::TNever, _) => true,
1433 (_, Atomic::TMixed) => true,
1435 (Atomic::TMixed, _) => true,
1436 (_, Atomic::TTemplateParam { as_type, .. }) => {
1441 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1442 }
1443
1444 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1446 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1447 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1448 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1449 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1450 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1451 (Atomic::TPositiveInt, Atomic::TInt) => true,
1452 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1453 (Atomic::TPositiveInt, Atomic::TNumeric) => true,
1454 (Atomic::TPositiveInt, Atomic::TScalar) => true,
1455 (Atomic::TNegativeInt, Atomic::TInt) => true,
1456 (Atomic::TNegativeInt, Atomic::TNumeric) => true,
1457 (Atomic::TNegativeInt, Atomic::TScalar) => true,
1458 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1459 (Atomic::TNonNegativeInt, Atomic::TNumeric) => true,
1460 (Atomic::TNonNegativeInt, Atomic::TScalar) => true,
1461 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1462 (Atomic::TIntRange { .. }, Atomic::TNumeric) => true,
1463 (Atomic::TIntRange { .. }, Atomic::TScalar) => true,
1464 (Atomic::TPositiveInt, Atomic::TIntRange { min, max }) => {
1466 max.is_none() && min.is_none_or(|m| m <= 1)
1467 }
1468 (Atomic::TNegativeInt, Atomic::TIntRange { min, max }) => {
1470 min.is_none() && max.is_none_or(|m| m >= -1)
1471 }
1472 (Atomic::TNonNegativeInt, Atomic::TIntRange { min, max }) => {
1474 max.is_none() && min.is_none_or(|m| m <= 0)
1475 }
1476 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TPositiveInt) => {
1478 sub_min.is_some_and(|lo| lo >= 1)
1479 }
1480 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TNonNegativeInt) => {
1481 sub_min.is_some_and(|lo| lo >= 0)
1482 }
1483 (Atomic::TIntRange { max: sub_max, .. }, Atomic::TNegativeInt) => {
1484 sub_max.is_some_and(|hi| hi <= -1)
1485 }
1486 (
1488 Atomic::TIntRange {
1489 min: sub_min,
1490 max: sub_max,
1491 },
1492 Atomic::TIntRange {
1493 min: sup_min,
1494 max: sup_max,
1495 },
1496 ) => {
1497 let lower_ok = match (sub_min, sup_min) {
1498 (_, None) => true,
1499 (None, Some(_)) => false,
1500 (Some(sl), Some(su)) => sl >= su,
1501 };
1502 let upper_ok = match (sub_max, sup_max) {
1503 (None, None) | (Some(_), None) => true,
1504 (None, Some(_)) => false,
1505 (Some(sl), Some(su)) => sl <= su,
1506 };
1507 lower_ok && upper_ok
1508 }
1509
1510 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1511 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1512 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1513
1514 (Atomic::TLiteralString(s), Atomic::TString) => {
1515 let _ = s;
1516 true
1517 }
1518 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1519 let _ = s;
1520 true
1521 }
1522 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1523 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1524 (Atomic::TLiteralString(_), Atomic::TClassString(_)) => true,
1527 (Atomic::TLiteralString(_), Atomic::TInterfaceString(_)) => true,
1530 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1531 (Atomic::TNonEmptyString, Atomic::TString) => true,
1532 (Atomic::TCallableString, Atomic::TString) => true,
1533 (Atomic::TNumericString, Atomic::TNonEmptyString) => true,
1535 (Atomic::TNumericString, Atomic::TString) => true,
1536 (Atomic::TClassString(_), Atomic::TString) => true,
1537 (Atomic::TInterfaceString(_), Atomic::TString) => true,
1538 (Atomic::TInterfaceString(_), Atomic::TClassString(None)) => true,
1543 (Atomic::TInterfaceString(Some(a)), Atomic::TClassString(Some(b))) => a == b,
1544 (Atomic::TEnumString, Atomic::TString) => true,
1545 (Atomic::TTraitString, Atomic::TString) => true,
1546
1547 (Atomic::TTrue, Atomic::TBool) => true,
1548 (Atomic::TFalse, Atomic::TBool) => true,
1549
1550 (Atomic::TInt, Atomic::TNumeric) => true,
1551 (Atomic::TFloat, Atomic::TNumeric) => true,
1552 (Atomic::TIntegralFloat, Atomic::TNumeric) => true,
1553 (Atomic::TNumericString, Atomic::TNumeric) => true,
1554
1555 (Atomic::TInt, Atomic::TScalar) => true,
1556 (Atomic::TFloat, Atomic::TScalar) => true,
1557 (Atomic::TIntegralFloat, Atomic::TScalar) => true,
1558 (Atomic::TString, Atomic::TScalar) => true,
1559 (Atomic::TBool, Atomic::TScalar) => true,
1560 (Atomic::TNumeric, Atomic::TScalar) => true,
1561 (Atomic::TTrue, Atomic::TScalar) => true,
1562 (Atomic::TFalse, Atomic::TScalar) => true,
1563
1564 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1566 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1567 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1568 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1570 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1571 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1573 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1574 (
1578 Atomic::TNamedObject {
1579 fqcn: sub_fqcn,
1580 type_params: sub_params,
1581 },
1582 Atomic::TNamedObject {
1583 fqcn: sup_fqcn,
1584 type_params: sup_params,
1585 },
1586 ) => {
1587 sub_fqcn == sup_fqcn
1588 && (sup_params.is_empty() || type_params_compatible(sub_params, sup_params))
1589 }
1590
1591 (Atomic::TIntegralFloat, Atomic::TFloat) => true,
1593
1594 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1596 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1597 (Atomic::TNegativeInt, Atomic::TFloat) => true,
1598 (Atomic::TNonNegativeInt, Atomic::TFloat) => true,
1599 (Atomic::TInt, Atomic::TFloat) => true,
1600 (Atomic::TIntRange { .. }, Atomic::TFloat) => true,
1601
1602 (Atomic::TLiteralInt(n), Atomic::TIntRange { min, max }) => {
1604 min.is_none_or(|lo| *n >= lo) && max.is_none_or(|hi| *n <= hi)
1605 }
1606
1607 (Atomic::TString, Atomic::TCallable { .. }) => true,
1609 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1610 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1611 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1612 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1613 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1614
1615 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1617 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1619 (Atomic::TClosure { data: sub }, Atomic::TClosure { data: sup }) => {
1629 fn has_nominal_type(t: &Type) -> bool {
1630 t.types.iter().any(|a| {
1631 matches!(
1632 a,
1633 Atomic::TNamedObject { .. }
1634 | Atomic::TSelf { .. }
1635 | Atomic::TStaticObject { .. }
1636 | Atomic::TTemplateParam { .. }
1637 | Atomic::TClosure { .. }
1638 | Atomic::TCallable { .. }
1639 )
1640 })
1641 }
1642 let sub_required = sub
1643 .params
1644 .iter()
1645 .filter(|p| !p.is_optional && !p.is_variadic)
1646 .count();
1647 if sub_required > sup.params.len() {
1648 false
1649 } else {
1650 let params_ok = sup.params.iter().enumerate().all(|(i, sup_param)| {
1651 let Some(sub_param) = sub.params.get(i) else {
1652 return true;
1653 };
1654 if sub_param.is_optional || sub_param.is_variadic {
1655 return true;
1656 }
1657 let (Some(sub_ty), Some(sup_ty)) =
1658 (sub_param.ty.as_ref(), sup_param.ty.as_ref())
1659 else {
1660 return true;
1661 };
1662 let (sub_u, sup_u) = (sub_ty.to_union(), sup_ty.to_union());
1663 if has_nominal_type(&sub_u) || has_nominal_type(&sup_u) {
1664 return true;
1665 }
1666 sup_u.is_subtype_structural(&sub_u)
1669 });
1670 params_ok
1671 && (sub.return_type.is_mixed()
1672 || sup.return_type.is_mixed()
1673 || has_nominal_type(&sub.return_type)
1674 || has_nominal_type(&sup.return_type)
1675 || sub.return_type.is_subtype_structural(&sup.return_type))
1676 }
1677 }
1678 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1680 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1682 fqcn.as_ref().eq_ignore_ascii_case("closure")
1683 }
1684 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1685 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1687 fqcn.as_ref().eq_ignore_ascii_case("closure")
1688 }
1689 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1691 fqcn.as_ref().eq_ignore_ascii_case("closure")
1692 }
1693
1694 (
1702 Atomic::TIntersection { parts: sub_parts },
1703 Atomic::TIntersection { parts: sup_parts },
1704 ) => sup_parts.iter().all(|sup_part| {
1705 sub_parts
1706 .iter()
1707 .any(|sub_part| sub_part.is_subtype_structural(sup_part))
1708 }),
1709
1710 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1712 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1713 }
1714 (Atomic::TNonEmptyList { value }, Atomic::TArray { key, value: av }) => {
1715 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1716 }
1717 (Atomic::TNonEmptyList { value }, Atomic::TNonEmptyArray { key, value: av }) => {
1718 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1719 }
1720 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1721 value.is_subtype_structural(lv)
1722 }
1723 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1725 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1726 && av.is_subtype_structural(lv)
1727 }
1728 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1729 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1730 && av.is_subtype_structural(lv)
1731 }
1732 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1733 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1734 && av.is_subtype_structural(lv)
1735 }
1736 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1737 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1738 && av.is_subtype_structural(lv)
1739 }
1740 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1742 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1743 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1744 }
1745
1746 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1748 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1749 }
1750
1751 (
1757 Atomic::TKeyedArray {
1758 properties,
1759 is_open,
1760 ..
1761 },
1762 Atomic::TArray { key, value },
1763 ) => {
1764 if *is_open {
1765 return true;
1766 }
1767 properties.iter().all(|(prop_key, prop)| {
1768 let key_atomic = match prop_key {
1769 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1770 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1771 };
1772 if !Type::single(key_atomic).is_subtype_structural(key) {
1773 return false; }
1775 let has_named_obj = prop.ty.types.iter().any(|a| {
1777 matches!(
1778 a,
1779 Atomic::TNamedObject { .. }
1780 | Atomic::TSelf { .. }
1781 | Atomic::TStaticObject { .. }
1782 | Atomic::TClosure { .. }
1783 | Atomic::TTemplateParam { .. }
1784 )
1785 });
1786 has_named_obj || prop.ty.is_subtype_structural(value)
1787 })
1788 }
1789 (
1790 Atomic::TKeyedArray {
1791 properties,
1792 is_open,
1793 ..
1794 },
1795 Atomic::TNonEmptyArray { key, value },
1796 ) => {
1797 if *is_open {
1798 return !properties.is_empty();
1799 }
1800 properties.iter().any(|(_, p)| !p.optional)
1801 && properties.iter().all(|(prop_key, prop)| {
1802 let key_atomic = match prop_key {
1803 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1804 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1805 };
1806 if !Type::single(key_atomic).is_subtype_structural(key) {
1807 return false;
1808 }
1809 let has_named_obj = prop.ty.types.iter().any(|a| {
1810 matches!(
1811 a,
1812 Atomic::TNamedObject { .. }
1813 | Atomic::TSelf { .. }
1814 | Atomic::TStaticObject { .. }
1815 | Atomic::TClosure { .. }
1816 | Atomic::TTemplateParam { .. }
1817 )
1818 });
1819 has_named_obj || prop.ty.is_subtype_structural(value)
1820 })
1821 }
1822
1823 (
1825 Atomic::TKeyedArray {
1826 properties,
1827 is_list,
1828 ..
1829 },
1830 Atomic::TList { value: lv },
1831 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1832 (
1833 Atomic::TKeyedArray {
1834 properties,
1835 is_list,
1836 ..
1837 },
1838 Atomic::TNonEmptyList { value: lv },
1839 ) => {
1840 *is_list
1841 && !properties.is_empty()
1842 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1843 }
1844
1845 (
1850 Atomic::TKeyedArray {
1851 properties: sub_props,
1852 is_open: sub_open,
1853 ..
1854 },
1855 Atomic::TKeyedArray {
1856 properties: sup_props,
1857 is_open: sup_open,
1858 ..
1859 },
1860 ) => {
1861 let keys_satisfied = sup_props
1862 .iter()
1863 .all(|(key, sup_prop)| match sub_props.get(key) {
1864 Some(sub_prop) => {
1865 let has_named_obj = sup_prop.ty.types.iter().any(|a| {
1866 matches!(
1867 a,
1868 Atomic::TNamedObject { .. }
1869 | Atomic::TSelf { .. }
1870 | Atomic::TStaticObject { .. }
1871 | Atomic::TClosure { .. }
1872 | Atomic::TTemplateParam { .. }
1873 )
1874 });
1875 has_named_obj || sub_prop.ty.is_subtype_structural(&sup_prop.ty)
1876 }
1877 None => sup_prop.optional || *sub_open,
1878 });
1879 let no_undeclared_extras =
1880 *sup_open || sub_props.keys().all(|k| sup_props.contains_key(k));
1881 keys_satisfied && no_undeclared_extras
1882 }
1883
1884 _ => false,
1885 }
1886}
1887
1888fn type_params_compatible(sub: &[Type], sup: &[Type]) -> bool {
1894 if sub.len() != sup.len() {
1895 return false;
1896 }
1897 sub.iter()
1898 .zip(sup.iter())
1899 .all(|(a, b)| a == b || (is_empty_array_literal(a) && is_array_like(b)))
1900}
1901
1902fn is_empty_array_literal(t: &Type) -> bool {
1905 !t.types.is_empty()
1906 && t.types.iter().all(
1907 |atom| matches!(atom, Atomic::TKeyedArray { properties, .. } if properties.is_empty()),
1908 )
1909}
1910
1911fn is_array_like(t: &Type) -> bool {
1913 !t.types.is_empty() && t.types.iter().all(|atom| atom.is_array())
1914}
1915
1916#[cfg(test)]
1921mod tests {
1922 use std::sync::Arc;
1923
1924 use super::*;
1925
1926 fn conditional(
1927 param_name: Option<Name>,
1928 subject: Type,
1929 if_true: Type,
1930 if_false: Type,
1931 ) -> Atomic {
1932 Atomic::TConditional {
1933 data: Box::new(crate::atomic::ConditionalData {
1934 param_name,
1935 subject,
1936 if_true,
1937 if_false,
1938 }),
1939 }
1940 }
1941
1942 #[test]
1943 fn single_is_single() {
1944 let u = Type::single(Atomic::TString);
1945 assert!(u.is_single());
1946 assert!(!u.is_nullable());
1947 }
1948
1949 #[test]
1950 fn nullable_has_null() {
1951 let u = Type::nullable(Atomic::TString);
1952 assert!(u.is_nullable());
1953 assert_eq!(u.types.len(), 2);
1954 }
1955
1956 #[test]
1957 fn add_type_deduplicates() {
1958 let mut u = Type::single(Atomic::TString);
1959 u.add_type(Atomic::TString);
1960 assert_eq!(u.types.len(), 1);
1961 }
1962
1963 #[test]
1964 fn array_key_is_int_string() {
1965 let k = Type::array_key();
1966 assert!(k.is_array_key());
1967 assert_eq!(k.types.len(), 2);
1968 }
1969
1970 #[test]
1971 fn is_array_key_false_for_plain_int() {
1972 assert!(!Type::int().is_array_key());
1973 }
1974
1975 #[test]
1976 fn is_array_key_false_for_mixed() {
1977 assert!(!Type::mixed().is_array_key());
1978 }
1979
1980 #[test]
1981 fn is_array_key_false_for_int_string_null() {
1982 let mut u = Type::array_key();
1983 u.add_type(Atomic::TNull);
1984 assert!(!u.is_array_key());
1985 }
1986
1987 #[test]
1988 fn add_type_literal_subsumed_by_base() {
1989 let mut u = Type::single(Atomic::TInt);
1990 u.add_type(Atomic::TLiteralInt(42));
1991 assert_eq!(u.types.len(), 1);
1992 assert!(matches!(u.types[0], Atomic::TInt));
1993 }
1994
1995 #[test]
1996 fn true_then_false_merges_to_bool() {
1997 let mut u = Type::single(Atomic::TTrue);
1998 u.add_type(Atomic::TFalse);
1999 assert_eq!(u.types.len(), 1);
2000 assert!(matches!(u.types[0], Atomic::TBool));
2001 }
2002
2003 #[test]
2004 fn false_then_true_merges_to_bool() {
2005 let mut u = Type::single(Atomic::TFalse);
2006 u.add_type(Atomic::TTrue);
2007 assert_eq!(u.types.len(), 1);
2008 assert!(matches!(u.types[0], Atomic::TBool));
2009 }
2010
2011 #[test]
2012 fn true_alone_stays_true() {
2013 let u = Type::single(Atomic::TTrue);
2014 assert_eq!(u.types.len(), 1);
2015 assert!(matches!(u.types[0], Atomic::TTrue));
2016 }
2017
2018 #[test]
2019 fn true_false_merge_preserves_other_union_members() {
2020 let mut u = Type::single(Atomic::TTrue);
2021 u.add_type(Atomic::TNull);
2022 u.add_type(Atomic::TFalse);
2023 assert_eq!(u.types.len(), 2);
2024 assert!(u.contains(|t| matches!(t, Atomic::TBool)));
2025 assert!(u.contains(|t| matches!(t, Atomic::TNull)));
2026 }
2027
2028 #[test]
2029 fn add_type_base_widens_literals() {
2030 let mut u = Type::single(Atomic::TLiteralInt(1));
2031 u.add_type(Atomic::TLiteralInt(2));
2032 u.add_type(Atomic::TInt);
2033 assert_eq!(u.types.len(), 1);
2034 assert!(matches!(u.types[0], Atomic::TInt));
2035 }
2036
2037 #[test]
2038 fn mixed_subsumes_everything() {
2039 let mut u = Type::single(Atomic::TString);
2040 u.add_type(Atomic::TMixed);
2041 assert_eq!(u.types.len(), 1);
2042 assert!(u.is_mixed());
2043 }
2044
2045 #[test]
2046 fn remove_null() {
2047 let u = Type::nullable(Atomic::TString);
2048 let narrowed = u.remove_null();
2049 assert!(!narrowed.is_nullable());
2050 assert_eq!(narrowed.types.len(), 1);
2051 }
2052
2053 #[test]
2054 fn narrow_to_truthy_removes_null_false() {
2055 let mut u = Type::empty();
2056 u.add_type(Atomic::TString);
2057 u.add_type(Atomic::TNull);
2058 u.add_type(Atomic::TFalse);
2059 let truthy = u.narrow_to_truthy();
2060 assert!(!truthy.is_nullable());
2061 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
2062 }
2063
2064 #[test]
2065 fn merge_combines_types() {
2066 let a = Type::single(Atomic::TString);
2067 let b = Type::single(Atomic::TInt);
2068 let merged = Type::merge(&a, &b);
2069 assert_eq!(merged.types.len(), 2);
2070 }
2071
2072 #[test]
2073 fn intersect_keeps_narrower_side_not_self() {
2074 let int_ty = Type::single(Atomic::TInt);
2078 let mut literals = Type::empty();
2079 literals.add_type(Atomic::TLiteralInt(1));
2080 literals.add_type(Atomic::TLiteralInt(2));
2081
2082 let narrowed = int_ty.intersect_with(&literals);
2083 assert_eq!(narrowed.types.len(), 2);
2084 assert!(narrowed.contains(|t| matches!(t, Atomic::TLiteralInt(1))));
2085 assert!(narrowed.contains(|t| matches!(t, Atomic::TLiteralInt(2))));
2086 assert!(!narrowed.contains(|t| matches!(t, Atomic::TInt)));
2087 }
2088
2089 #[test]
2090 fn subtype_literal_int_under_int() {
2091 let sub = Type::single(Atomic::TLiteralInt(5));
2092 let sup = Type::single(Atomic::TInt);
2093 assert!(sub.is_subtype_structural(&sup));
2094 }
2095
2096 #[test]
2097 fn subtype_never_is_bottom() {
2098 let never = Type::never();
2099 let string = Type::single(Atomic::TString);
2100 assert!(never.is_subtype_structural(&string));
2101 }
2102
2103 #[test]
2104 fn subtype_everything_under_mixed() {
2105 let string = Type::single(Atomic::TString);
2106 let mixed = Type::mixed();
2107 assert!(string.is_subtype_structural(&mixed));
2108 }
2109
2110 #[test]
2111 fn template_substitution() {
2112 let mut bindings = FxHashMap::default();
2113 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
2114
2115 let tmpl = Type::single(Atomic::TTemplateParam {
2116 name: Name::new("T"),
2117 as_type: Box::new(Type::mixed()),
2118 defining_entity: Name::new("MyClass"),
2119 });
2120
2121 let resolved = tmpl.substitute_templates(&bindings);
2122 assert_eq!(resolved.types.len(), 1);
2123 assert!(matches!(resolved.types[0], Atomic::TString));
2124 }
2125
2126 #[test]
2127 fn intersection_is_object() {
2128 let parts = vec![
2129 Type::single(Atomic::TNamedObject {
2130 fqcn: Name::new("Iterator"),
2131 type_params: empty_type_params(),
2132 }),
2133 Type::single(Atomic::TNamedObject {
2134 fqcn: Name::new("Countable"),
2135 type_params: empty_type_params(),
2136 }),
2137 ];
2138 let atomic = Atomic::TIntersection {
2139 parts: vec_to_type_params(parts),
2140 };
2141 assert!(atomic.is_object());
2142 assert!(!atomic.can_be_falsy());
2143 assert!(atomic.can_be_truthy());
2144 }
2145
2146 #[test]
2147 fn intersection_display_two_parts() {
2148 let parts = vec![
2149 Type::single(Atomic::TNamedObject {
2150 fqcn: Name::new("Iterator"),
2151 type_params: empty_type_params(),
2152 }),
2153 Type::single(Atomic::TNamedObject {
2154 fqcn: Name::new("Countable"),
2155 type_params: empty_type_params(),
2156 }),
2157 ];
2158 let u = Type::single(Atomic::TIntersection {
2159 parts: vec_to_type_params(parts),
2160 });
2161 assert_eq!(format!("{u}"), "Iterator&Countable");
2162 }
2163
2164 #[test]
2165 fn intersection_display_three_parts() {
2166 let parts = vec![
2167 Type::single(Atomic::TNamedObject {
2168 fqcn: Name::new("A"),
2169 type_params: empty_type_params(),
2170 }),
2171 Type::single(Atomic::TNamedObject {
2172 fqcn: Name::new("B"),
2173 type_params: empty_type_params(),
2174 }),
2175 Type::single(Atomic::TNamedObject {
2176 fqcn: Name::new("C"),
2177 type_params: empty_type_params(),
2178 }),
2179 ];
2180 let u = Type::single(Atomic::TIntersection {
2181 parts: vec_to_type_params(parts),
2182 });
2183 assert_eq!(format!("{u}"), "A&B&C");
2184 }
2185
2186 #[test]
2187 fn intersection_in_nullable_union_display() {
2188 let intersection = Atomic::TIntersection {
2189 parts: vec_to_type_params(vec![
2190 Type::single(Atomic::TNamedObject {
2191 fqcn: Name::new("Iterator"),
2192 type_params: empty_type_params(),
2193 }),
2194 Type::single(Atomic::TNamedObject {
2195 fqcn: Name::new("Countable"),
2196 type_params: empty_type_params(),
2197 }),
2198 ]),
2199 };
2200 let mut u = Type::single(intersection);
2201 u.add_type(Atomic::TNull);
2202 assert!(u.is_nullable());
2203 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
2204 }
2205
2206 fn t_param(name: &str) -> Type {
2209 Type::single(Atomic::TTemplateParam {
2210 name: Name::new(name),
2211 as_type: Box::new(Type::mixed()),
2212 defining_entity: Name::new("Fn"),
2213 })
2214 }
2215
2216 fn bindings_t_string() -> FxHashMap<Name, Type> {
2217 let mut b = FxHashMap::default();
2218 b.insert(Name::new("T"), Type::single(Atomic::TString));
2219 b
2220 }
2221
2222 #[test]
2223 fn substitute_non_empty_array_key_and_value() {
2224 let ty = Type::single(Atomic::TNonEmptyArray {
2225 key: Box::new(t_param("T")),
2226 value: Box::new(t_param("T")),
2227 });
2228 let result = ty.substitute_templates(&bindings_t_string());
2229 assert_eq!(result.types.len(), 1);
2230 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
2231 panic!("expected TNonEmptyArray");
2232 };
2233 assert!(matches!(key.types[0], Atomic::TString));
2234 assert!(matches!(value.types[0], Atomic::TString));
2235 }
2236
2237 #[test]
2238 fn substitute_non_empty_list_value() {
2239 let ty = Type::single(Atomic::TNonEmptyList {
2240 value: Box::new(t_param("T")),
2241 });
2242 let result = ty.substitute_templates(&bindings_t_string());
2243 let Atomic::TNonEmptyList { value } = &result.types[0] else {
2244 panic!("expected TNonEmptyList");
2245 };
2246 assert!(matches!(value.types[0], Atomic::TString));
2247 }
2248
2249 #[test]
2250 fn substitute_keyed_array_property_types() {
2251 use crate::atomic::{ArrayKey, KeyedProperty};
2252 use indexmap::IndexMap;
2253 let mut props = IndexMap::new();
2254 props.insert(
2255 ArrayKey::String(Arc::from("name")),
2256 KeyedProperty {
2257 ty: t_param("T"),
2258 optional: false,
2259 },
2260 );
2261 props.insert(
2262 ArrayKey::String(Arc::from("tag")),
2263 KeyedProperty {
2264 ty: t_param("T"),
2265 optional: true,
2266 },
2267 );
2268 let ty = Type::single(Atomic::TKeyedArray {
2269 properties: Box::new(props),
2270 is_open: true,
2271 is_list: false,
2272 });
2273 let result = ty.substitute_templates(&bindings_t_string());
2274 let Atomic::TKeyedArray {
2275 properties,
2276 is_open,
2277 is_list,
2278 } = &result.types[0]
2279 else {
2280 panic!("expected TKeyedArray");
2281 };
2282 assert!(is_open);
2283 assert!(!is_list);
2284 assert!(matches!(
2285 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
2286 Atomic::TString
2287 ));
2288 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
2289 assert!(matches!(
2290 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
2291 Atomic::TString
2292 ));
2293 }
2294
2295 #[test]
2296 fn substitute_callable_params_and_return() {
2297 use crate::atomic::FnParam;
2298 let ty = Type::single(Atomic::TCallable {
2299 params: Some(Box::new([FnParam {
2300 name: Name::new("x"),
2301 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2302 out_ty: None,
2303 default: None,
2304 is_variadic: false,
2305 is_byref: false,
2306 is_optional: false,
2307 }])),
2308 return_type: Some(Box::new(t_param("T"))),
2309 });
2310 let result = ty.substitute_templates(&bindings_t_string());
2311 let Atomic::TCallable {
2312 params,
2313 return_type,
2314 } = &result.types[0]
2315 else {
2316 panic!("expected TCallable");
2317 };
2318 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
2319 let param_union = param_ty.to_union();
2320 assert!(matches!(param_union.types[0], Atomic::TString));
2321 let ret = return_type.as_ref().unwrap();
2322 assert!(matches!(ret.types[0], Atomic::TString));
2323 }
2324
2325 #[test]
2326 fn substitute_callable_bare_no_panic() {
2327 let ty = Type::single(Atomic::TCallable {
2329 params: None,
2330 return_type: None,
2331 });
2332 let result = ty.substitute_templates(&bindings_t_string());
2333 assert!(matches!(
2334 result.types[0],
2335 Atomic::TCallable {
2336 params: None,
2337 return_type: None
2338 }
2339 ));
2340 }
2341
2342 #[test]
2343 fn substitute_closure_params_return_and_this() {
2344 use crate::atomic::FnParam;
2345 let ty = Type::single(Atomic::TClosure {
2346 data: Box::new(crate::atomic::ClosureData {
2347 params: Box::new([FnParam {
2348 name: Name::new("a"),
2349 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2350 out_ty: None,
2351 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
2352 is_variadic: true,
2353 is_byref: true,
2354 is_optional: true,
2355 }]),
2356 return_type: t_param("T"),
2357 this_type: Some(t_param("T")),
2358 }),
2359 });
2360 let result = ty.substitute_templates(&bindings_t_string());
2361 let Atomic::TClosure { data } = &result.types[0] else {
2362 panic!("expected TClosure");
2363 };
2364 let (params, return_type, this_type) = (&data.params, &data.return_type, &data.this_type);
2365 let p = ¶ms[0];
2366 let ty_union = p.ty.as_ref().unwrap().to_union();
2367 let default_union = p.default.as_ref().unwrap().to_union();
2368 assert!(matches!(ty_union.types[0], Atomic::TString));
2369 assert!(matches!(default_union.types[0], Atomic::TString));
2370 assert!(p.is_variadic);
2372 assert!(p.is_byref);
2373 assert!(p.is_optional);
2374 assert!(matches!(return_type.types[0], Atomic::TString));
2375 assert!(matches!(
2376 this_type.as_ref().unwrap().types[0],
2377 Atomic::TString
2378 ));
2379 }
2380
2381 #[test]
2382 fn substitute_conditional_all_branches() {
2383 let ty = Type::single(conditional(
2384 None,
2385 t_param("T"),
2386 t_param("T"),
2387 Type::single(Atomic::TInt),
2388 ));
2389 let result = ty.substitute_templates(&bindings_t_string());
2390 let Atomic::TConditional { data } = &result.types[0] else {
2391 panic!("expected TConditional");
2392 };
2393 let (subject, if_true, if_false) = (&data.subject, &data.if_true, &data.if_false);
2394 assert!(matches!(subject.types[0], Atomic::TString));
2395 assert!(matches!(if_true.types[0], Atomic::TString));
2396 assert!(matches!(if_false.types[0], Atomic::TInt));
2397 }
2398
2399 #[test]
2400 fn resolve_conditional_is_null_non_null_arg() {
2401 let ty = Type::single(conditional(
2402 Some(Name::new("x")),
2403 Type::single(Atomic::TNull),
2404 Type::single(Atomic::TInt),
2405 Type::single(Atomic::TString),
2406 ));
2407 let result = ty.resolve_conditional_returns(|name| {
2408 if name == "x" {
2409 Some(Type::single(Atomic::TString)) } else {
2411 None
2412 }
2413 });
2414 assert!(result.types.len() == 1);
2415 assert!(matches!(result.types[0], Atomic::TString));
2416 }
2417
2418 #[test]
2419 fn resolve_conditional_is_null_null_arg() {
2420 let ty = Type::single(conditional(
2421 Some(Name::new("x")),
2422 Type::single(Atomic::TNull),
2423 Type::single(Atomic::TInt),
2424 Type::single(Atomic::TString),
2425 ));
2426 let result = ty.resolve_conditional_returns(|name| {
2427 if name == "x" {
2428 Some(Type::single(Atomic::TNull)) } else {
2430 None
2431 }
2432 });
2433 assert!(result.types.len() == 1);
2434 assert!(matches!(result.types[0], Atomic::TInt));
2435 }
2436
2437 #[test]
2438 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
2439 let mut nullable_str = Type::single(Atomic::TString);
2440 nullable_str.add_type(Atomic::TNull);
2441 let ty = Type::single(conditional(
2442 Some(Name::new("x")),
2443 Type::single(Atomic::TNull),
2444 Type::single(Atomic::TInt),
2445 Type::single(Atomic::TString),
2446 ));
2447 let result = ty.resolve_conditional_returns(|name| {
2448 if name == "x" {
2449 Some(nullable_str.clone())
2450 } else {
2451 None
2452 }
2453 });
2454 assert_eq!(result.types.len(), 2);
2456 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2457 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2458 }
2459
2460 #[test]
2461 fn resolve_conditional_nested_widens_inner_branch() {
2462 let inner = Type::single(conditional(
2465 Some(Name::new("x")),
2466 Type::single(Atomic::TString),
2467 Type::single(Atomic::TString),
2468 Type::single(Atomic::TFloat),
2469 ));
2470 let ty = Type::single(conditional(
2471 Some(Name::new("x")),
2472 Type::single(Atomic::TNull),
2473 Type::single(Atomic::TInt),
2474 inner,
2475 ));
2476 let result = ty.resolve_conditional_returns(|_| None);
2478 assert!(
2479 result
2480 .types
2481 .iter()
2482 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2483 "no TConditional should survive: {:?}",
2484 result.types
2485 );
2486 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2487 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2488 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
2489 }
2490
2491 #[test]
2492 fn resolve_conditional_nested_resolves_inner_branch() {
2493 let inner = Type::single(conditional(
2497 Some(Name::new("x")),
2498 Type::single(Atomic::TString),
2499 Type::single(Atomic::TString),
2500 Type::single(Atomic::TFloat),
2501 ));
2502 let ty = Type::single(conditional(
2503 Some(Name::new("x")),
2504 Type::single(Atomic::TNull),
2505 Type::single(Atomic::TInt),
2506 inner,
2507 ));
2508 let result = ty.resolve_conditional_returns(|name| {
2510 if name == "x" {
2511 Some(Type::single(Atomic::TString))
2512 } else {
2513 None
2514 }
2515 });
2516 assert!(
2517 result
2518 .types
2519 .iter()
2520 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2521 "no TConditional should survive: {:?}",
2522 result.types
2523 );
2524 assert_eq!(result.types.len(), 1);
2525 assert!(matches!(result.types[0], Atomic::TString));
2526 }
2527
2528 #[test]
2529 fn substitute_intersection_parts() {
2530 let ty = Type::single(Atomic::TIntersection {
2531 parts: vec_to_type_params(vec![
2532 Type::single(Atomic::TNamedObject {
2533 fqcn: Name::new("Countable"),
2534 type_params: empty_type_params(),
2535 }),
2536 t_param("T"),
2537 ]),
2538 });
2539 let result = ty.substitute_templates(&bindings_t_string());
2540 let Atomic::TIntersection { parts } = &result.types[0] else {
2541 panic!("expected TIntersection");
2542 };
2543 assert_eq!(parts.len(), 2);
2544 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
2545 assert!(matches!(parts[1].types[0], Atomic::TString));
2546 }
2547
2548 #[test]
2549 fn substitute_no_template_params_identity() {
2550 let ty = Type::single(Atomic::TInt);
2551 let result = ty.substitute_templates(&bindings_t_string());
2552 assert!(matches!(result.types[0], Atomic::TInt));
2553 }
2554}