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