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, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct Type {
36 pub types: AtomicVec,
37 pub possibly_undefined: bool,
39 pub from_docblock: bool,
41}
42
43impl Type {
44 pub fn empty() -> Self {
47 Self {
48 types: SmallVec::new(),
49 possibly_undefined: false,
50 from_docblock: false,
51 }
52 }
53
54 pub fn single(atomic: Atomic) -> Self {
55 let mut types = SmallVec::new();
56 types.push(atomic);
57 Self {
58 types,
59 possibly_undefined: false,
60 from_docblock: false,
61 }
62 }
63
64 pub fn mixed() -> Self {
65 Self::single(Atomic::TMixed)
66 }
67
68 pub fn void() -> Self {
69 Self::single(Atomic::TVoid)
70 }
71
72 pub fn never() -> Self {
73 Self::single(Atomic::TNever)
74 }
75
76 pub fn null() -> Self {
77 Self::single(Atomic::TNull)
78 }
79
80 pub fn bool() -> Self {
81 Self::single(Atomic::TBool)
82 }
83
84 pub fn int() -> Self {
85 Self::single(Atomic::TInt)
86 }
87
88 pub fn float() -> Self {
89 Self::single(Atomic::TFloat)
90 }
91
92 pub fn string() -> Self {
93 Self::single(Atomic::TString)
94 }
95
96 pub fn nullable(atomic: Atomic) -> Self {
98 let mut types = SmallVec::new();
99 types.push(atomic);
100 types.push(Atomic::TNull);
101 Self {
102 types,
103 possibly_undefined: false,
104 from_docblock: false,
105 }
106 }
107
108 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
110 let mut u = Self::empty();
111 for a in atomics {
112 u.add_type(a);
113 }
114 u
115 }
116
117 pub fn is_empty(&self) -> bool {
120 self.types.is_empty()
121 }
122
123 pub fn is_single(&self) -> bool {
124 self.types.len() == 1
125 }
126
127 pub fn is_nullable(&self) -> bool {
128 self.types.iter().any(|t| matches!(t, Atomic::TNull))
129 }
130
131 pub fn is_mixed(&self) -> bool {
132 self.types.iter().any(|t| match t {
133 Atomic::TMixed => true,
134 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
135 _ => false,
136 })
137 }
138
139 pub fn is_never(&self) -> bool {
140 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
141 }
142
143 pub fn is_void(&self) -> bool {
144 self.is_single() && matches!(self.types[0], Atomic::TVoid)
145 }
146
147 pub fn can_be_falsy(&self) -> bool {
148 self.types.iter().any(|t| t.can_be_falsy())
149 }
150
151 pub fn can_be_truthy(&self) -> bool {
152 self.types.iter().any(|t| t.can_be_truthy())
153 }
154
155 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
156 self.types.iter().any(f)
157 }
158
159 pub fn has_named_object(&self, fqcn: &str) -> bool {
160 self.types.iter().any(|t| match t {
161 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
162 _ => false,
163 })
164 }
165
166 pub fn add_type(&mut self, atomic: Atomic) {
171 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
173 return;
174 }
175
176 if matches!(atomic, Atomic::TMixed) {
178 self.types.clear();
179 self.types.push(Atomic::TMixed);
180 return;
181 }
182
183 let atomic = if let Atomic::TConditional {
186 param_name: _,
187 subject: _,
188 if_true,
189 if_false,
190 } = &atomic
191 {
192 let mut simplified_true = Type::empty();
193 for t in &if_true.types {
194 simplified_true.add_type(t.clone());
195 }
196 let mut simplified_false = Type::empty();
197 for t in &if_false.types {
198 simplified_false.add_type(t.clone());
199 }
200 if simplified_true == simplified_false {
201 for t in simplified_true.types {
202 self.add_type(t);
203 }
204 return;
205 }
206 atomic
207 } else {
208 atomic
209 };
210
211 if self.types.contains(&atomic) {
213 return;
214 }
215
216 if let Atomic::TLiteralInt(_) = &atomic {
218 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
219 return;
220 }
221 }
222 if let Atomic::TLiteralString(_) = &atomic {
224 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
225 return;
226 }
227 }
228 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
230 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
231 {
232 return;
233 }
234 if matches!(atomic, Atomic::TInt) {
236 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
237 }
238 if matches!(atomic, Atomic::TString) {
240 self.types
241 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
242 }
243 if matches!(atomic, Atomic::TBool) {
245 self.types
246 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
247 }
248
249 if matches!(atomic, Atomic::TNever) {
251 if !self.types.is_empty() {
252 return;
253 }
254 } else {
255 self.types.retain(|t| !matches!(t, Atomic::TNever));
256 }
257
258 if let Atomic::TKeyedArray { properties, .. } = &atomic {
261 if properties.is_empty() {
262 for existing in &self.types {
264 match existing {
265 Atomic::TArray { .. } | Atomic::TNonEmptyArray { .. } => {
266 return; }
268 _ => {}
269 }
270 }
271 }
272 }
273
274 let is_generic_array = matches!(
276 &atomic,
277 Atomic::TArray { .. } | Atomic::TNonEmptyArray { .. }
278 );
279 if is_generic_array {
280 self.types.retain(|t| {
281 if let Atomic::TKeyedArray { properties, .. } = t {
282 !properties.is_empty()
283 } else {
284 true
285 }
286 });
287 }
288
289 self.types.push(atomic);
290 }
291
292 pub fn remove_null(&self) -> Type {
296 self.filter(|t| !matches!(t, Atomic::TNull))
297 }
298
299 pub fn remove_false(&self) -> Type {
301 self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
302 }
303
304 pub fn core_type(&self) -> Type {
306 self.remove_null().remove_false()
307 }
308
309 pub fn narrow_to_truthy(&self) -> Type {
311 if self.is_mixed() {
312 return Type::mixed();
313 }
314 let narrowed = self.filter(|t| t.can_be_truthy());
315 narrowed.filter(|t| match t {
317 Atomic::TLiteralInt(0) => false,
318 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
319 Atomic::TLiteralFloat(0, 0) => false,
320 _ => true,
321 })
322 }
323
324 pub fn narrow_to_falsy(&self) -> Type {
326 if self.is_mixed() {
327 return Type::from_vec(vec![
328 Atomic::TNull,
329 Atomic::TFalse,
330 Atomic::TLiteralInt(0),
331 Atomic::TLiteralString("".into()),
332 ]);
333 }
334 self.filter(|t| t.can_be_falsy())
335 }
336
337 pub fn narrow_instanceof(&self, class: &str) -> Type {
343 let narrowed_ty = Atomic::TNamedObject {
344 fqcn: class.into(),
345 type_params: empty_type_params(),
346 };
347 let has_object = self.types.iter().any(|t| {
349 matches!(
350 t,
351 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
353 });
354 if has_object || self.is_empty() {
355 Type::single(narrowed_ty)
356 } else {
357 Type::single(narrowed_ty)
360 }
361 }
362
363 pub fn narrow_to_string(&self) -> Type {
365 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
366 }
367
368 pub fn narrow_to_int(&self) -> Type {
370 self.filter(|t| {
371 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
372 })
373 }
374
375 pub fn narrow_to_float(&self) -> Type {
377 self.filter(|t| {
378 matches!(
379 t,
380 Atomic::TFloat
381 | Atomic::TLiteralFloat(..)
382 | Atomic::TMixed
383 | Atomic::TScalar
384 | Atomic::TNumeric
385 )
386 })
387 }
388
389 pub fn narrow_to_bool(&self) -> Type {
391 self.filter(|t| {
392 matches!(
393 t,
394 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
395 )
396 })
397 }
398
399 pub fn narrow_to_null(&self) -> Type {
401 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
402 }
403
404 pub fn narrow_to_array(&self) -> Type {
406 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
407 }
408
409 pub fn narrow_to_object(&self) -> Type {
411 self.filter(|t| t.is_object() || matches!(t, Atomic::TMixed))
412 }
413
414 pub fn narrow_to_callable(&self) -> Type {
416 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
417 }
418
419 pub fn narrow_to_scalar(&self) -> Type {
421 self.filter(|t| {
422 matches!(
423 t,
424 Atomic::TString
425 | Atomic::TLiteralString(..)
426 | Atomic::TNumericString
427 | Atomic::TInt
428 | Atomic::TLiteralInt(..)
429 | Atomic::TFloat
430 | Atomic::TLiteralFloat(..)
431 | Atomic::TBool
432 | Atomic::TTrue
433 | Atomic::TFalse
434 | Atomic::TScalar
435 | Atomic::TMixed
436 )
437 })
438 }
439
440 pub fn narrow_to_iterable(&self) -> Type {
443 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
444 }
445
446 pub fn narrow_to_countable(&self) -> Type {
449 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
450 }
451
452 pub fn narrow_to_resource(&self) -> Type {
456 self.filter(|t| matches!(t, Atomic::TMixed))
458 }
459
460 pub fn merge(a: &Type, b: &Type) -> Type {
465 if b.types.is_empty() {
467 let mut result = a.clone();
468 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
469 return result;
470 }
471 if a.types.is_empty() {
473 let mut result = b.clone();
474 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
475 return result;
476 }
477 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
479 let mut result = a.clone();
480 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
481 return result;
482 }
483 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
485 return Type {
486 types: smallvec::smallvec![Atomic::TMixed],
487 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
488 from_docblock: a.from_docblock || b.from_docblock,
489 };
490 }
491 let mut result = a.clone();
492 result.merge_with(b);
493 result
494 }
495
496 pub fn merge_with(&mut self, other: &Type) {
498 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
499 self.possibly_undefined |= other.possibly_undefined;
500 return;
501 }
502 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
503 self.types.clear();
504 self.types.push(Atomic::TMixed);
505 self.possibly_undefined |= other.possibly_undefined;
506 return;
507 }
508 for atomic in &other.types {
509 self.add_type(atomic.clone());
510 }
511 self.possibly_undefined |= other.possibly_undefined;
512 }
513
514 pub fn intersect_with(&self, other: &Type) -> Type {
518 if self.is_mixed() {
519 return other.clone();
520 }
521 if other.is_mixed() {
522 return self.clone();
523 }
524 let mut result = Type::empty();
526 for a in &self.types {
527 for b in &other.types {
528 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
529 result.add_type(a.clone());
530 break;
531 }
532 }
533 }
534 if result.is_empty() {
535 Type::never()
536 } else {
537 result
538 }
539 }
540
541 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
545 if bindings.is_empty() {
546 return self.clone();
547 }
548 let mut result = Type::empty();
549 result.possibly_undefined = self.possibly_undefined;
550 result.from_docblock = self.from_docblock;
551 for atomic in &self.types {
552 match atomic {
553 Atomic::TTemplateParam { name, .. } => {
554 if let Some(resolved) = bindings.get(name) {
555 for t in &resolved.types {
556 result.add_type(t.clone());
557 }
558 } else {
559 result.add_type(atomic.clone());
560 }
561 }
562 Atomic::TArray { key, value } => {
563 result.add_type(Atomic::TArray {
564 key: Box::new(key.substitute_templates(bindings)),
565 value: Box::new(value.substitute_templates(bindings)),
566 });
567 }
568 Atomic::TList { value } => {
569 result.add_type(Atomic::TList {
570 value: Box::new(value.substitute_templates(bindings)),
571 });
572 }
573 Atomic::TNonEmptyArray { key, value } => {
574 result.add_type(Atomic::TNonEmptyArray {
575 key: Box::new(key.substitute_templates(bindings)),
576 value: Box::new(value.substitute_templates(bindings)),
577 });
578 }
579 Atomic::TNonEmptyList { value } => {
580 result.add_type(Atomic::TNonEmptyList {
581 value: Box::new(value.substitute_templates(bindings)),
582 });
583 }
584 Atomic::TKeyedArray {
585 properties,
586 is_open,
587 is_list,
588 } => {
589 use crate::atomic::KeyedProperty;
590 let new_props = properties
591 .iter()
592 .map(|(k, prop)| {
593 (
594 k.clone(),
595 KeyedProperty {
596 ty: prop.ty.substitute_templates(bindings),
597 optional: prop.optional,
598 },
599 )
600 })
601 .collect();
602 result.add_type(Atomic::TKeyedArray {
603 properties: new_props,
604 is_open: *is_open,
605 is_list: *is_list,
606 });
607 }
608 Atomic::TCallable {
609 params,
610 return_type,
611 } => {
612 result.add_type(Atomic::TCallable {
613 params: params.as_ref().map(|ps| {
614 ps.iter()
615 .map(|p| substitute_in_fn_param(p, bindings))
616 .collect()
617 }),
618 return_type: return_type
619 .as_ref()
620 .map(|r| Box::new(r.substitute_templates(bindings))),
621 });
622 }
623 Atomic::TClosure {
624 params,
625 return_type,
626 this_type,
627 } => {
628 result.add_type(Atomic::TClosure {
629 params: params
630 .iter()
631 .map(|p| substitute_in_fn_param(p, bindings))
632 .collect(),
633 return_type: Box::new(return_type.substitute_templates(bindings)),
634 this_type: this_type
635 .as_ref()
636 .map(|t| Box::new(t.substitute_templates(bindings))),
637 });
638 }
639 Atomic::TConditional {
640 param_name,
641 subject,
642 if_true,
643 if_false,
644 } => {
645 let new_subject = subject.substitute_templates(bindings);
646 let new_if_true = if_true.substitute_templates(bindings);
647 let new_if_false = if_false.substitute_templates(bindings);
648
649 let resolved = if let Some(name) = param_name {
653 if let Some(bound) = bindings.get(name) {
654 let subject_is_null = new_subject.types.len() == 1
655 && matches!(new_subject.types[0], Atomic::TNull);
656 if subject_is_null {
657 let only_null = !bound.types.is_empty()
658 && bound.types.iter().all(|t| matches!(t, Atomic::TNull));
659 let has_null =
660 bound.types.iter().any(|t| matches!(t, Atomic::TNull));
661 if only_null {
662 Some(new_if_true.clone())
663 } else if !has_null {
664 Some(new_if_false.clone())
665 } else {
666 None
667 }
668 } else {
669 None
670 }
671 } else {
672 None
673 }
674 } else {
675 None
676 };
677
678 if let Some(branch) = resolved {
679 for t in branch.types {
680 result.add_type(t);
681 }
682 } else {
683 result.add_type(Atomic::TConditional {
684 param_name: *param_name,
685 subject: Box::new(new_subject),
686 if_true: Box::new(new_if_true),
687 if_false: Box::new(new_if_false),
688 });
689 }
690 }
691 Atomic::TIntersection { parts } => {
692 result.add_type(Atomic::TIntersection {
693 parts: vec_to_type_params(
694 parts
695 .iter()
696 .map(|p| p.substitute_templates(bindings))
697 .collect(),
698 ),
699 });
700 }
701 Atomic::TNamedObject { fqcn, type_params } => {
702 if type_params.is_empty() && !fqcn.contains('\\') {
709 if let Some(resolved) = bindings.get(fqcn) {
710 for t in &resolved.types {
711 result.add_type(t.clone());
712 }
713 continue;
714 }
715 }
716 let new_params: Vec<Type> = type_params
717 .iter()
718 .map(|p| p.substitute_templates(bindings))
719 .collect();
720 result.add_type(Atomic::TNamedObject {
721 fqcn: *fqcn,
722 type_params: vec_to_type_params(new_params),
723 });
724 }
725 Atomic::TClassString(Some(param_name)) => {
727 if let Some(resolved) = bindings.get(param_name) {
728 for r_atomic in &resolved.types {
729 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
730 Some(*fqcn)
731 } else {
732 None
733 };
734 result.add_type(Atomic::TClassString(cls_name));
735 }
736 } else {
737 result.add_type(atomic.clone());
738 }
739 }
740 _ => {
741 result.add_type(atomic.clone());
742 }
743 }
744 }
745 result
746 }
747
748 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
754 where
755 F: Fn(&str) -> Option<Type>,
756 {
757 let mut result = Type::empty();
758 for atomic in self.types {
759 match atomic {
760 Atomic::TConditional {
761 ref param_name,
762 ref subject,
763 ref if_true,
764 ref if_false,
765 } => {
766 let subject_is_null =
768 subject.types.len() == 1 && matches!(subject.types[0], Atomic::TNull);
769 let resolved = if subject_is_null {
770 if let Some(name) = param_name {
771 if let Some(arg_ty) = lookup(name.as_ref()) {
772 let has_null =
773 arg_ty.types.iter().any(|t| matches!(t, Atomic::TNull));
774 let only_null = !arg_ty.types.is_empty()
775 && arg_ty.types.iter().all(|t| matches!(t, Atomic::TNull));
776 if only_null {
777 Some((**if_true).clone())
778 } else if !has_null {
779 Some((**if_false).clone())
780 } else {
781 None
782 }
783 } else {
784 None
785 }
786 } else {
787 None
788 }
789 } else {
790 None
791 };
792
793 if let Some(branch) = resolved {
794 for t in branch.types {
795 result.add_type(t);
796 }
797 } else {
798 for t in if_true.types.iter() {
801 result.add_type(t.clone());
802 }
803 for t in if_false.types.iter() {
804 result.add_type(t.clone());
805 }
806 }
807 }
808 other => result.add_type(other),
809 }
810 }
811 result
812 }
813
814 pub fn is_subtype_structural(&self, other: &Type) -> bool {
824 if other.is_mixed() {
825 return true;
826 }
827 if self.is_never() {
828 return true; }
830 self.types
831 .iter()
832 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
833 }
834
835 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
838 let mut result = Type::empty();
839 result.possibly_undefined = self.possibly_undefined;
840 result.from_docblock = self.from_docblock;
841 for atomic in &self.types {
842 if f(atomic) {
843 result.types.push(atomic.clone());
844 }
845 }
846 result
847 }
848
849 pub fn possibly_undefined(mut self) -> Self {
851 self.possibly_undefined = true;
852 self
853 }
854
855 pub fn from_docblock(mut self) -> Self {
857 self.from_docblock = true;
858 self
859 }
860}
861
862fn substitute_in_fn_param(
867 p: &crate::atomic::FnParam,
868 bindings: &FxHashMap<Name, Type>,
869) -> crate::atomic::FnParam {
870 crate::atomic::FnParam {
871 name: p.name,
872 ty: p.ty.as_ref().map(|t| {
873 let u = t.to_union();
874 let substituted = u.substitute_templates(bindings);
875 crate::compact::SimpleType::from_union(substituted)
876 }),
877 default: p.default.as_ref().map(|d| {
878 let u = d.to_union();
879 let substituted = u.substitute_templates(bindings);
880 crate::compact::SimpleType::from_union(substituted)
881 }),
882 is_variadic: p.is_variadic,
883 is_byref: p.is_byref,
884 is_optional: p.is_optional,
885 }
886}
887
888fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
893 if sub == sup {
894 return true;
895 }
896 match (sub, sup) {
897 (Atomic::TNever, _) => true,
899 (_, Atomic::TMixed) => true,
901 (Atomic::TMixed, _) => true,
902
903 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
905 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
906 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
907 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
908 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
909 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
910 (Atomic::TPositiveInt, Atomic::TInt) => true,
911 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
912 (Atomic::TNegativeInt, Atomic::TInt) => true,
913 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
914 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
915
916 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
917 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
918 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
919
920 (Atomic::TLiteralString(s), Atomic::TString) => {
921 let _ = s;
922 true
923 }
924 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
925 let _ = s;
926 true
927 }
928 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
929 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
930 (Atomic::TNonEmptyString, Atomic::TString) => true,
931 (Atomic::TCallableString, Atomic::TString) => true,
932 (Atomic::TNumericString, Atomic::TString) => true,
933 (Atomic::TClassString(_), Atomic::TString) => true,
934 (Atomic::TInterfaceString, Atomic::TString) => true,
935 (Atomic::TEnumString, Atomic::TString) => true,
936 (Atomic::TTraitString, Atomic::TString) => true,
937
938 (Atomic::TTrue, Atomic::TBool) => true,
939 (Atomic::TFalse, Atomic::TBool) => true,
940
941 (Atomic::TInt, Atomic::TNumeric) => true,
942 (Atomic::TFloat, Atomic::TNumeric) => true,
943 (Atomic::TNumericString, Atomic::TNumeric) => true,
944
945 (Atomic::TInt, Atomic::TScalar) => true,
946 (Atomic::TFloat, Atomic::TScalar) => true,
947 (Atomic::TString, Atomic::TScalar) => true,
948 (Atomic::TBool, Atomic::TScalar) => true,
949 (Atomic::TNumeric, Atomic::TScalar) => true,
950 (Atomic::TTrue, Atomic::TScalar) => true,
951 (Atomic::TFalse, Atomic::TScalar) => true,
952
953 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
955 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
956 (Atomic::TSelf { .. }, Atomic::TObject) => true,
957 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
959 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
960 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
962 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
963 (
967 Atomic::TNamedObject {
968 fqcn: sub_fqcn,
969 type_params: sub_params,
970 },
971 Atomic::TNamedObject {
972 fqcn: sup_fqcn,
973 type_params: sup_params,
974 },
975 ) => sub_fqcn == sup_fqcn && (sup_params.is_empty() || sub_params == sup_params),
976
977 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
979 (Atomic::TPositiveInt, Atomic::TFloat) => true,
980 (Atomic::TInt, Atomic::TFloat) => true,
981
982 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
984
985 (Atomic::TString, Atomic::TCallable { .. }) => true,
987 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
988 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
989 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
990 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
991
992 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
994 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
996 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
998 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1000 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1002 fqcn.as_ref().eq_ignore_ascii_case("closure")
1003 }
1004 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1005
1006 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1008 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1010 && value.is_subtype_structural(av)
1011 }
1012 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1013 value.is_subtype_structural(lv)
1014 }
1015 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1017 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1018 && av.is_subtype_structural(lv)
1019 }
1020 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1021 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1022 && av.is_subtype_structural(lv)
1023 }
1024 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1025 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1026 && av.is_subtype_structural(lv)
1027 }
1028 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1029 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1030 && av.is_subtype_structural(lv)
1031 }
1032 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1034 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1035 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1036 }
1037
1038 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1040 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1041 }
1042
1043 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
1045
1046 (
1048 Atomic::TKeyedArray {
1049 properties,
1050 is_list,
1051 ..
1052 },
1053 Atomic::TList { value: lv },
1054 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1055 (
1056 Atomic::TKeyedArray {
1057 properties,
1058 is_list,
1059 ..
1060 },
1061 Atomic::TNonEmptyList { value: lv },
1062 ) => {
1063 *is_list
1064 && !properties.is_empty()
1065 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1066 }
1067
1068 (_, Atomic::TTemplateParam { .. }) => true,
1070
1071 _ => false,
1072 }
1073}
1074
1075#[cfg(test)]
1080mod tests {
1081 use std::sync::Arc;
1082
1083 use super::*;
1084
1085 #[test]
1086 fn single_is_single() {
1087 let u = Type::single(Atomic::TString);
1088 assert!(u.is_single());
1089 assert!(!u.is_nullable());
1090 }
1091
1092 #[test]
1093 fn nullable_has_null() {
1094 let u = Type::nullable(Atomic::TString);
1095 assert!(u.is_nullable());
1096 assert_eq!(u.types.len(), 2);
1097 }
1098
1099 #[test]
1100 fn add_type_deduplicates() {
1101 let mut u = Type::single(Atomic::TString);
1102 u.add_type(Atomic::TString);
1103 assert_eq!(u.types.len(), 1);
1104 }
1105
1106 #[test]
1107 fn add_type_literal_subsumed_by_base() {
1108 let mut u = Type::single(Atomic::TInt);
1109 u.add_type(Atomic::TLiteralInt(42));
1110 assert_eq!(u.types.len(), 1);
1111 assert!(matches!(u.types[0], Atomic::TInt));
1112 }
1113
1114 #[test]
1115 fn add_type_base_widens_literals() {
1116 let mut u = Type::single(Atomic::TLiteralInt(1));
1117 u.add_type(Atomic::TLiteralInt(2));
1118 u.add_type(Atomic::TInt);
1119 assert_eq!(u.types.len(), 1);
1120 assert!(matches!(u.types[0], Atomic::TInt));
1121 }
1122
1123 #[test]
1124 fn mixed_subsumes_everything() {
1125 let mut u = Type::single(Atomic::TString);
1126 u.add_type(Atomic::TMixed);
1127 assert_eq!(u.types.len(), 1);
1128 assert!(u.is_mixed());
1129 }
1130
1131 #[test]
1132 fn remove_null() {
1133 let u = Type::nullable(Atomic::TString);
1134 let narrowed = u.remove_null();
1135 assert!(!narrowed.is_nullable());
1136 assert_eq!(narrowed.types.len(), 1);
1137 }
1138
1139 #[test]
1140 fn narrow_to_truthy_removes_null_false() {
1141 let mut u = Type::empty();
1142 u.add_type(Atomic::TString);
1143 u.add_type(Atomic::TNull);
1144 u.add_type(Atomic::TFalse);
1145 let truthy = u.narrow_to_truthy();
1146 assert!(!truthy.is_nullable());
1147 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1148 }
1149
1150 #[test]
1151 fn merge_combines_types() {
1152 let a = Type::single(Atomic::TString);
1153 let b = Type::single(Atomic::TInt);
1154 let merged = Type::merge(&a, &b);
1155 assert_eq!(merged.types.len(), 2);
1156 }
1157
1158 #[test]
1159 fn subtype_literal_int_under_int() {
1160 let sub = Type::single(Atomic::TLiteralInt(5));
1161 let sup = Type::single(Atomic::TInt);
1162 assert!(sub.is_subtype_structural(&sup));
1163 }
1164
1165 #[test]
1166 fn subtype_never_is_bottom() {
1167 let never = Type::never();
1168 let string = Type::single(Atomic::TString);
1169 assert!(never.is_subtype_structural(&string));
1170 }
1171
1172 #[test]
1173 fn subtype_everything_under_mixed() {
1174 let string = Type::single(Atomic::TString);
1175 let mixed = Type::mixed();
1176 assert!(string.is_subtype_structural(&mixed));
1177 }
1178
1179 #[test]
1180 fn template_substitution() {
1181 let mut bindings = FxHashMap::default();
1182 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1183
1184 let tmpl = Type::single(Atomic::TTemplateParam {
1185 name: Name::new("T"),
1186 as_type: Box::new(Type::mixed()),
1187 defining_entity: Name::new("MyClass"),
1188 });
1189
1190 let resolved = tmpl.substitute_templates(&bindings);
1191 assert_eq!(resolved.types.len(), 1);
1192 assert!(matches!(resolved.types[0], Atomic::TString));
1193 }
1194
1195 #[test]
1196 fn intersection_is_object() {
1197 let parts = vec![
1198 Type::single(Atomic::TNamedObject {
1199 fqcn: Name::new("Iterator"),
1200 type_params: empty_type_params(),
1201 }),
1202 Type::single(Atomic::TNamedObject {
1203 fqcn: Name::new("Countable"),
1204 type_params: empty_type_params(),
1205 }),
1206 ];
1207 let atomic = Atomic::TIntersection {
1208 parts: vec_to_type_params(parts),
1209 };
1210 assert!(atomic.is_object());
1211 assert!(!atomic.can_be_falsy());
1212 assert!(atomic.can_be_truthy());
1213 }
1214
1215 #[test]
1216 fn intersection_display_two_parts() {
1217 let parts = vec![
1218 Type::single(Atomic::TNamedObject {
1219 fqcn: Name::new("Iterator"),
1220 type_params: empty_type_params(),
1221 }),
1222 Type::single(Atomic::TNamedObject {
1223 fqcn: Name::new("Countable"),
1224 type_params: empty_type_params(),
1225 }),
1226 ];
1227 let u = Type::single(Atomic::TIntersection {
1228 parts: vec_to_type_params(parts),
1229 });
1230 assert_eq!(format!("{u}"), "Iterator&Countable");
1231 }
1232
1233 #[test]
1234 fn intersection_display_three_parts() {
1235 let parts = vec![
1236 Type::single(Atomic::TNamedObject {
1237 fqcn: Name::new("A"),
1238 type_params: empty_type_params(),
1239 }),
1240 Type::single(Atomic::TNamedObject {
1241 fqcn: Name::new("B"),
1242 type_params: empty_type_params(),
1243 }),
1244 Type::single(Atomic::TNamedObject {
1245 fqcn: Name::new("C"),
1246 type_params: empty_type_params(),
1247 }),
1248 ];
1249 let u = Type::single(Atomic::TIntersection {
1250 parts: vec_to_type_params(parts),
1251 });
1252 assert_eq!(format!("{u}"), "A&B&C");
1253 }
1254
1255 #[test]
1256 fn intersection_in_nullable_union_display() {
1257 let intersection = Atomic::TIntersection {
1258 parts: vec_to_type_params(vec![
1259 Type::single(Atomic::TNamedObject {
1260 fqcn: Name::new("Iterator"),
1261 type_params: empty_type_params(),
1262 }),
1263 Type::single(Atomic::TNamedObject {
1264 fqcn: Name::new("Countable"),
1265 type_params: empty_type_params(),
1266 }),
1267 ]),
1268 };
1269 let mut u = Type::single(intersection);
1270 u.add_type(Atomic::TNull);
1271 assert!(u.is_nullable());
1272 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1273 }
1274
1275 fn t_param(name: &str) -> Type {
1278 Type::single(Atomic::TTemplateParam {
1279 name: Name::new(name),
1280 as_type: Box::new(Type::mixed()),
1281 defining_entity: Name::new("Fn"),
1282 })
1283 }
1284
1285 fn bindings_t_string() -> FxHashMap<Name, Type> {
1286 let mut b = FxHashMap::default();
1287 b.insert(Name::new("T"), Type::single(Atomic::TString));
1288 b
1289 }
1290
1291 #[test]
1292 fn substitute_non_empty_array_key_and_value() {
1293 let ty = Type::single(Atomic::TNonEmptyArray {
1294 key: Box::new(t_param("T")),
1295 value: Box::new(t_param("T")),
1296 });
1297 let result = ty.substitute_templates(&bindings_t_string());
1298 assert_eq!(result.types.len(), 1);
1299 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1300 panic!("expected TNonEmptyArray");
1301 };
1302 assert!(matches!(key.types[0], Atomic::TString));
1303 assert!(matches!(value.types[0], Atomic::TString));
1304 }
1305
1306 #[test]
1307 fn substitute_non_empty_list_value() {
1308 let ty = Type::single(Atomic::TNonEmptyList {
1309 value: Box::new(t_param("T")),
1310 });
1311 let result = ty.substitute_templates(&bindings_t_string());
1312 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1313 panic!("expected TNonEmptyList");
1314 };
1315 assert!(matches!(value.types[0], Atomic::TString));
1316 }
1317
1318 #[test]
1319 fn substitute_keyed_array_property_types() {
1320 use crate::atomic::{ArrayKey, KeyedProperty};
1321 use indexmap::IndexMap;
1322 let mut props = IndexMap::new();
1323 props.insert(
1324 ArrayKey::String(Arc::from("name")),
1325 KeyedProperty {
1326 ty: t_param("T"),
1327 optional: false,
1328 },
1329 );
1330 props.insert(
1331 ArrayKey::String(Arc::from("tag")),
1332 KeyedProperty {
1333 ty: t_param("T"),
1334 optional: true,
1335 },
1336 );
1337 let ty = Type::single(Atomic::TKeyedArray {
1338 properties: props,
1339 is_open: true,
1340 is_list: false,
1341 });
1342 let result = ty.substitute_templates(&bindings_t_string());
1343 let Atomic::TKeyedArray {
1344 properties,
1345 is_open,
1346 is_list,
1347 } = &result.types[0]
1348 else {
1349 panic!("expected TKeyedArray");
1350 };
1351 assert!(is_open);
1352 assert!(!is_list);
1353 assert!(matches!(
1354 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1355 Atomic::TString
1356 ));
1357 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1358 assert!(matches!(
1359 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1360 Atomic::TString
1361 ));
1362 }
1363
1364 #[test]
1365 fn substitute_callable_params_and_return() {
1366 use crate::atomic::FnParam;
1367 let ty = Type::single(Atomic::TCallable {
1368 params: Some(vec![FnParam {
1369 name: Name::new("x"),
1370 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1371 default: None,
1372 is_variadic: false,
1373 is_byref: false,
1374 is_optional: false,
1375 }]),
1376 return_type: Some(Box::new(t_param("T"))),
1377 });
1378 let result = ty.substitute_templates(&bindings_t_string());
1379 let Atomic::TCallable {
1380 params,
1381 return_type,
1382 } = &result.types[0]
1383 else {
1384 panic!("expected TCallable");
1385 };
1386 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1387 let param_union = param_ty.to_union();
1388 assert!(matches!(param_union.types[0], Atomic::TString));
1389 let ret = return_type.as_ref().unwrap();
1390 assert!(matches!(ret.types[0], Atomic::TString));
1391 }
1392
1393 #[test]
1394 fn substitute_callable_bare_no_panic() {
1395 let ty = Type::single(Atomic::TCallable {
1397 params: None,
1398 return_type: None,
1399 });
1400 let result = ty.substitute_templates(&bindings_t_string());
1401 assert!(matches!(
1402 result.types[0],
1403 Atomic::TCallable {
1404 params: None,
1405 return_type: None
1406 }
1407 ));
1408 }
1409
1410 #[test]
1411 fn substitute_closure_params_return_and_this() {
1412 use crate::atomic::FnParam;
1413 let ty = Type::single(Atomic::TClosure {
1414 params: vec![FnParam {
1415 name: Name::new("a"),
1416 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1417 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1418 is_variadic: true,
1419 is_byref: true,
1420 is_optional: true,
1421 }],
1422 return_type: Box::new(t_param("T")),
1423 this_type: Some(Box::new(t_param("T"))),
1424 });
1425 let result = ty.substitute_templates(&bindings_t_string());
1426 let Atomic::TClosure {
1427 params,
1428 return_type,
1429 this_type,
1430 } = &result.types[0]
1431 else {
1432 panic!("expected TClosure");
1433 };
1434 let p = ¶ms[0];
1435 let ty_union = p.ty.as_ref().unwrap().to_union();
1436 let default_union = p.default.as_ref().unwrap().to_union();
1437 assert!(matches!(ty_union.types[0], Atomic::TString));
1438 assert!(matches!(default_union.types[0], Atomic::TString));
1439 assert!(p.is_variadic);
1441 assert!(p.is_byref);
1442 assert!(p.is_optional);
1443 assert!(matches!(return_type.types[0], Atomic::TString));
1444 assert!(matches!(
1445 this_type.as_ref().unwrap().types[0],
1446 Atomic::TString
1447 ));
1448 }
1449
1450 #[test]
1451 fn substitute_conditional_all_branches() {
1452 let ty = Type::single(Atomic::TConditional {
1453 param_name: None,
1454 subject: Box::new(t_param("T")),
1455 if_true: Box::new(t_param("T")),
1456 if_false: Box::new(Type::single(Atomic::TInt)),
1457 });
1458 let result = ty.substitute_templates(&bindings_t_string());
1459 let Atomic::TConditional {
1460 param_name: _,
1461 subject,
1462 if_true,
1463 if_false,
1464 } = &result.types[0]
1465 else {
1466 panic!("expected TConditional");
1467 };
1468 assert!(matches!(subject.types[0], Atomic::TString));
1469 assert!(matches!(if_true.types[0], Atomic::TString));
1470 assert!(matches!(if_false.types[0], Atomic::TInt));
1471 }
1472
1473 #[test]
1474 fn resolve_conditional_is_null_non_null_arg() {
1475 let ty = Type::single(Atomic::TConditional {
1476 param_name: Some(Name::new("x")),
1477 subject: Box::new(Type::single(Atomic::TNull)),
1478 if_true: Box::new(Type::single(Atomic::TInt)),
1479 if_false: Box::new(Type::single(Atomic::TString)),
1480 });
1481 let result = ty.resolve_conditional_returns(|name| {
1482 if name == "x" {
1483 Some(Type::single(Atomic::TString)) } else {
1485 None
1486 }
1487 });
1488 assert!(result.types.len() == 1);
1489 assert!(matches!(result.types[0], Atomic::TString));
1490 }
1491
1492 #[test]
1493 fn resolve_conditional_is_null_null_arg() {
1494 let ty = Type::single(Atomic::TConditional {
1495 param_name: Some(Name::new("x")),
1496 subject: Box::new(Type::single(Atomic::TNull)),
1497 if_true: Box::new(Type::single(Atomic::TInt)),
1498 if_false: Box::new(Type::single(Atomic::TString)),
1499 });
1500 let result = ty.resolve_conditional_returns(|name| {
1501 if name == "x" {
1502 Some(Type::single(Atomic::TNull)) } else {
1504 None
1505 }
1506 });
1507 assert!(result.types.len() == 1);
1508 assert!(matches!(result.types[0], Atomic::TInt));
1509 }
1510
1511 #[test]
1512 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
1513 let mut nullable_str = Type::single(Atomic::TString);
1514 nullable_str.add_type(Atomic::TNull);
1515 let ty = Type::single(Atomic::TConditional {
1516 param_name: Some(Name::new("x")),
1517 subject: Box::new(Type::single(Atomic::TNull)),
1518 if_true: Box::new(Type::single(Atomic::TInt)),
1519 if_false: Box::new(Type::single(Atomic::TString)),
1520 });
1521 let result = ty.resolve_conditional_returns(|name| {
1522 if name == "x" {
1523 Some(nullable_str.clone())
1524 } else {
1525 None
1526 }
1527 });
1528 assert_eq!(result.types.len(), 2);
1530 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1531 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1532 }
1533
1534 #[test]
1535 fn substitute_intersection_parts() {
1536 let ty = Type::single(Atomic::TIntersection {
1537 parts: vec_to_type_params(vec![
1538 Type::single(Atomic::TNamedObject {
1539 fqcn: Name::new("Countable"),
1540 type_params: empty_type_params(),
1541 }),
1542 t_param("T"),
1543 ]),
1544 });
1545 let result = ty.substitute_templates(&bindings_t_string());
1546 let Atomic::TIntersection { parts } = &result.types[0] else {
1547 panic!("expected TIntersection");
1548 };
1549 assert_eq!(parts.len(), 2);
1550 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1551 assert!(matches!(parts[1].types[0], Atomic::TString));
1552 }
1553
1554 #[test]
1555 fn substitute_no_template_params_identity() {
1556 let ty = Type::single(Atomic::TInt);
1557 let result = ty.substitute_templates(&bindings_t_string());
1558 assert!(matches!(result.types[0], Atomic::TInt));
1559 }
1560}