Skip to main content

mago_codex/ttype/
union.rs

1use std::borrow::Cow;
2use std::hash::Hash;
3use std::hash::Hasher;
4use std::sync::Arc;
5
6use mago_word::Word;
7use mago_word::concat_word;
8use mago_word::empty_word;
9use mago_word::join_words;
10use mago_word::word;
11
12use crate::metadata::CodebaseMetadata;
13use crate::reference::ReferenceSource;
14use crate::reference::SymbolReferences;
15use crate::symbol::Symbols;
16use crate::ttype::TType;
17use crate::ttype::TypeRef;
18use crate::ttype::atomic::TAtomic;
19use crate::ttype::atomic::array::TArray;
20use crate::ttype::atomic::array::key::ArrayKey;
21use crate::ttype::atomic::generic::TGenericParameter;
22use crate::ttype::atomic::mixed::truthiness::TMixedTruthiness;
23use crate::ttype::atomic::object::TObject;
24use crate::ttype::atomic::object::named::TNamedObject;
25use crate::ttype::atomic::object::with_properties::TObjectWithProperties;
26use crate::ttype::atomic::populate_atomic_type;
27use crate::ttype::atomic::scalar::TScalar;
28use crate::ttype::atomic::scalar::bool::TBool;
29use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
30use crate::ttype::atomic::scalar::float::TFloat;
31use crate::ttype::atomic::scalar::int::TInteger;
32use crate::ttype::atomic::scalar::string::TString;
33use crate::ttype::atomic::scalar::string::TStringCasing;
34use crate::ttype::atomic::scalar::string::TStringLiteral;
35use crate::ttype::flags::UnionFlags;
36use crate::ttype::get_arraykey;
37use crate::ttype::get_int;
38use crate::ttype::get_mixed;
39
40#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct TUnion {
43    pub types: Cow<'static, [TAtomic]>,
44    pub flags: UnionFlags,
45}
46
47impl Hash for TUnion {
48    fn hash<H>(&self, state: &mut H)
49    where
50        H: Hasher,
51    {
52        for t in self.types.as_ref() {
53            t.hash(state);
54        }
55    }
56}
57
58impl TUnion {
59    /// The primary constructor for creating a `TUnion` from a Cow.
60    ///
61    /// This is the most basic way to create a `TUnion` and is used by both the
62    /// zero-allocation static helpers and the `from_vec` constructor.
63    #[must_use]
64    pub fn new(types: Cow<'static, [TAtomic]>) -> TUnion {
65        TUnion { types, flags: UnionFlags::empty() }
66    }
67
68    /// Creates a `TUnion` from an owned Vec, performing necessary cleanup.
69    ///
70    /// This preserves the original logic for cleaning up dynamically created unions,
71    /// such as removing redundant `never` types.
72    ///
73    /// Atoms are additionally sorted into canonical order so that two unions
74    /// built from the same set of atoms compare equal via the ordered
75    /// slice-equality fast path in [`PartialEq::eq`], without triggering the
76    /// fallback O(N^2) subset check.
77    ///
78    /// # Panics
79    ///
80    /// In debug builds, panics if:
81    /// - The input Vec is empty (unions must contain at least one type)
82    /// - The input contains a mix of `never` types with other types (invalid union construction)
83    #[must_use]
84    pub fn from_vec(mut types: Vec<TAtomic>) -> TUnion {
85        if cfg!(debug_assertions) {
86            assert!(
87                !types.is_empty(),
88                "TUnion::from_vec() received an empty Vec. This indicates a logic error \
89                 in type construction - unions must contain at least one type. \
90                 Consider using TAtomic::Never for empty/impossible types."
91            );
92        }
93
94        // If we have more than one type, 'never' is redundant and can be removed,
95        // as the union `A|never` is simply `A`.
96        if types.len() > 1 {
97            types.retain(|atomic| {
98                !atomic.is_never() && !atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
99            });
100        }
101
102        // If the vector was originally empty, or contained only 'never' types
103        // which were removed, ensure the final union is `never`.
104        if types.is_empty() {
105            types.push(TAtomic::Never);
106        }
107
108        if types.len() > 1 {
109            types.sort_unstable();
110        }
111
112        Self::new(Cow::Owned(types))
113    }
114
115    /// Creates a `TUnion` from a single atomic type, which can be either
116    /// borrowed from a static source or owned.
117    ///
118    /// This function is a key optimization point. When passed a `Cow::Borrowed`,
119    /// it creates the `TUnion` without any heap allocation.
120    #[must_use]
121    pub fn from_single(atomic: Cow<'static, TAtomic>) -> TUnion {
122        let types_cow = match atomic {
123            Cow::Borrowed(borrowed_atomic) => Cow::Borrowed(std::slice::from_ref(borrowed_atomic)),
124            Cow::Owned(owned_atomic) => Cow::Owned(vec![owned_atomic]),
125        };
126
127        TUnion::new(types_cow)
128    }
129
130    /// Creates a `TUnion` from a single owned atomic type.
131    #[must_use]
132    pub fn from_atomic(atomic: TAtomic) -> TUnion {
133        TUnion::new(Cow::Owned(vec![atomic]))
134    }
135
136    #[inline]
137    pub fn set_possibly_undefined(&mut self, possibly_undefined: bool, from_try: Option<bool>) {
138        let from_try = from_try.unwrap_or(self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY));
139
140        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED, possibly_undefined);
141        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, from_try);
142    }
143
144    #[inline]
145    #[must_use]
146    pub const fn had_template(&self) -> bool {
147        self.flags.contains(UnionFlags::HAD_TEMPLATE)
148    }
149
150    #[inline]
151    #[must_use]
152    pub const fn by_reference(&self) -> bool {
153        self.flags.contains(UnionFlags::BY_REFERENCE)
154    }
155
156    #[inline]
157    #[must_use]
158    pub const fn reference_free(&self) -> bool {
159        self.flags.contains(UnionFlags::REFERENCE_FREE)
160    }
161
162    #[inline]
163    #[must_use]
164    pub const fn possibly_undefined_from_try(&self) -> bool {
165        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
166    }
167
168    #[inline]
169    #[must_use]
170    pub const fn possibly_undefined(&self) -> bool {
171        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED)
172    }
173
174    #[inline]
175    #[must_use]
176    pub const fn ignore_nullable_issues(&self) -> bool {
177        self.flags.contains(UnionFlags::IGNORE_NULLABLE_ISSUES)
178    }
179
180    #[inline]
181    #[must_use]
182    pub const fn ignore_falsable_issues(&self) -> bool {
183        self.flags.contains(UnionFlags::IGNORE_FALSABLE_ISSUES)
184    }
185
186    #[inline]
187    #[must_use]
188    pub const fn from_template_default(&self) -> bool {
189        self.flags.contains(UnionFlags::FROM_TEMPLATE_DEFAULT)
190    }
191
192    #[inline]
193    #[must_use]
194    pub const fn populated(&self) -> bool {
195        self.flags.contains(UnionFlags::POPULATED)
196    }
197
198    #[inline]
199    #[must_use]
200    pub const fn has_nullsafe_null(&self) -> bool {
201        self.flags.contains(UnionFlags::NULLSAFE_NULL)
202    }
203
204    #[inline]
205    pub fn set_had_template(&mut self, value: bool) {
206        self.flags.set(UnionFlags::HAD_TEMPLATE, value);
207    }
208
209    #[inline]
210    pub fn set_by_reference(&mut self, value: bool) {
211        self.flags.set(UnionFlags::BY_REFERENCE, value);
212    }
213
214    #[inline]
215    pub fn set_reference_free(&mut self, value: bool) {
216        self.flags.set(UnionFlags::REFERENCE_FREE, value);
217    }
218
219    #[inline]
220    pub fn set_possibly_undefined_from_try(&mut self, value: bool) {
221        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, value);
222    }
223
224    #[inline]
225    pub fn set_ignore_nullable_issues(&mut self, value: bool) {
226        self.flags.set(UnionFlags::IGNORE_NULLABLE_ISSUES, value);
227    }
228
229    #[inline]
230    pub fn set_ignore_falsable_issues(&mut self, value: bool) {
231        self.flags.set(UnionFlags::IGNORE_FALSABLE_ISSUES, value);
232    }
233
234    #[inline]
235    pub fn set_from_template_default(&mut self, value: bool) {
236        self.flags.set(UnionFlags::FROM_TEMPLATE_DEFAULT, value);
237    }
238
239    #[inline]
240    pub fn set_populated(&mut self, value: bool) {
241        self.flags.set(UnionFlags::POPULATED, value);
242    }
243
244    #[inline]
245    pub fn set_nullsafe_null(&mut self, value: bool) {
246        self.flags.set(UnionFlags::NULLSAFE_NULL, value);
247    }
248
249    /// Creates a new `TUnion` with the same properties as the original, but with a new set of types.
250    #[must_use]
251    pub fn clone_with_types(&self, types: Vec<TAtomic>) -> TUnion {
252        TUnion { types: Cow::Owned(ensure_non_empty_types(types)), flags: self.flags }
253    }
254
255    #[must_use]
256    pub fn to_non_nullable(&self) -> TUnion {
257        TUnion {
258            types: Cow::Owned(ensure_non_empty_types(self.get_non_nullable_types())),
259            flags: self.flags & !UnionFlags::NULLSAFE_NULL,
260        }
261    }
262
263    #[must_use]
264    pub fn to_truthy(&self) -> TUnion {
265        TUnion { types: Cow::Owned(ensure_non_empty_types(self.get_truthy_types())), flags: self.flags }
266    }
267
268    #[must_use]
269    pub fn get_non_nullable_types(&self) -> Vec<TAtomic> {
270        self.types
271            .iter()
272            .filter_map(|t| match t {
273                TAtomic::Null | TAtomic::Void => None,
274                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
275                    parameter_name: parameter.parameter_name,
276                    defining_entity: parameter.defining_entity,
277                    intersection_types: parameter.intersection_types.clone(),
278                    constraint: Arc::new(parameter.constraint.to_non_nullable()),
279                })),
280                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_is_non_null(true))),
281                atomic => Some(atomic.clone()),
282            })
283            .collect()
284    }
285
286    #[must_use]
287    pub fn get_truthy_types(&self) -> Vec<TAtomic> {
288        self.types
289            .iter()
290            .filter_map(|t| match t {
291                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
292                    parameter_name: parameter.parameter_name,
293                    defining_entity: parameter.defining_entity,
294                    intersection_types: parameter.intersection_types.clone(),
295                    constraint: Arc::new(parameter.constraint.to_truthy()),
296                })),
297                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_truthiness(TMixedTruthiness::Truthy))),
298                atomic => {
299                    if atomic.is_falsy() {
300                        None
301                    } else {
302                        Some(atomic.clone())
303                    }
304                }
305            })
306            .collect()
307    }
308
309    /// Recursively replaces every narrowed scalar atom in this union with its
310    /// general form: any string narrowing -> `string`, any integer narrowing
311    /// (literal, range, non-negative-int, ...) -> `int`, any float narrowing
312    /// -> `float`, `true`/`false` -> `bool`.
313    ///
314    /// Descends into nested type holders (array elements, object type
315    /// parameters, iterable key/value, generic parameter constraints,
316    /// conditional branches, intersection types, ...) so a narrowing nested
317    /// arbitrarily deep in a callee-mutable structure is also widened.
318    pub fn widen_scalars(&mut self) {
319        for atomic in self.types.to_mut() {
320            widen_atomic_scalars(atomic);
321        }
322    }
323
324    /// Recursively replaces only *literal* scalar atoms in this union with
325    /// their general form: literal strings -> `string`, literal ints -> `int`,
326    /// literal floats -> `float`, `true`/`false` -> `bool`. Unlike
327    /// [`Self::widen_scalars`], user-declared narrowings such as
328    /// `non-negative-int`, `non-empty-string`, or `int<1, max>` are preserved.
329    pub fn widen_literals(&mut self) {
330        for atomic in self.types.to_mut() {
331            widen_atomic_literals(atomic);
332        }
333    }
334
335    /// Adds `null` to the union type, making it nullable.
336    #[must_use]
337    pub fn as_nullable(mut self) -> TUnion {
338        let types = self.types.to_mut();
339
340        for atomic in types.iter_mut() {
341            if let TAtomic::Mixed(mixed) = atomic {
342                *mixed = mixed.with_is_non_null(false);
343            }
344        }
345
346        if !types.iter().any(|atomic| atomic.is_null() || atomic.is_mixed()) {
347            types.push(TAtomic::Null);
348        }
349
350        self
351    }
352
353    /// Removes a specific atomic type from the union.
354    pub fn remove_type(&mut self, bad_type: &TAtomic) {
355        self.types.to_mut().retain(|t| t != bad_type);
356    }
357
358    /// Replaces a specific atomic type in the union with a new type.
359    pub fn replace_type(&mut self, remove_type: &TAtomic, add_type: TAtomic) {
360        let types = self.types.to_mut();
361
362        if let Some(index) = types.iter().position(|t| t == remove_type) {
363            types[index] = add_type;
364        } else {
365            types.push(add_type);
366        }
367    }
368
369    #[must_use]
370    pub fn is_int(&self) -> bool {
371        for atomic in self.types.as_ref() {
372            if !atomic.is_int() {
373                return false;
374            }
375        }
376
377        true
378    }
379
380    #[must_use]
381    pub fn has_int_or_float(&self) -> bool {
382        for atomic in self.types.as_ref() {
383            if atomic.is_int_or_float() {
384                return true;
385            }
386        }
387
388        false
389    }
390
391    #[must_use]
392    pub fn has_int_and_float(&self) -> bool {
393        let mut has_int = false;
394        let mut has_float = false;
395
396        for atomic in self.types.as_ref() {
397            if atomic.is_int() {
398                has_int = true;
399            } else if atomic.is_float() {
400                has_float = true;
401            } else if atomic.is_int_or_float() {
402                has_int = true;
403                has_float = true;
404            }
405
406            if has_int && has_float {
407                return true;
408            }
409        }
410
411        false
412    }
413
414    #[must_use]
415    pub fn has_int_and_string(&self) -> bool {
416        let mut has_int = false;
417        let mut has_string = false;
418
419        for atomic in self.types.as_ref() {
420            if atomic.is_int() {
421                has_int = true;
422            } else if atomic.is_string() {
423                has_string = true;
424            } else if atomic.is_array_key() {
425                has_int = true;
426                has_string = true;
427            }
428
429            if has_int && has_string {
430                return true;
431            }
432        }
433
434        false
435    }
436
437    #[must_use]
438    pub fn has_int(&self) -> bool {
439        for atomic in self.types.as_ref() {
440            if atomic.is_int() || atomic.is_array_key() || atomic.is_numeric() {
441                return true;
442            }
443        }
444
445        false
446    }
447
448    #[must_use]
449    pub fn has_float(&self) -> bool {
450        for atomic in self.types.as_ref() {
451            if atomic.is_float() {
452                return true;
453            }
454        }
455
456        false
457    }
458
459    #[must_use]
460    pub fn is_array_key(&self) -> bool {
461        for atomic in self.types.as_ref() {
462            if atomic.is_array_key() {
463                continue;
464            }
465
466            return false;
467        }
468
469        true
470    }
471
472    #[must_use]
473    pub fn is_any_string(&self) -> bool {
474        for atomic in self.types.as_ref() {
475            if !atomic.is_any_string() {
476                return false;
477            }
478        }
479
480        true
481    }
482
483    pub fn is_string(&self) -> bool {
484        self.types.iter().all(TAtomic::is_string) && !self.types.is_empty()
485    }
486
487    #[must_use]
488    pub fn is_always_array_key(&self, ignore_never: bool) -> bool {
489        self.types.iter().all(|atomic| match atomic {
490            TAtomic::Never => ignore_never,
491            TAtomic::Scalar(scalar) => matches!(
492                scalar,
493                TScalar::ArrayKey | TScalar::Integer(_) | TScalar::String(_) | TScalar::ClassLikeString(_)
494            ),
495            TAtomic::GenericParameter(generic_parameter) => {
496                generic_parameter.constraint.is_always_array_key(ignore_never)
497            }
498            _ => false,
499        })
500    }
501
502    pub fn is_non_empty_string(&self) -> bool {
503        self.types.iter().all(TAtomic::is_non_empty_string) && !self.types.is_empty()
504    }
505
506    pub fn is_empty_array(&self) -> bool {
507        self.types.iter().all(TAtomic::is_empty_array) && !self.types.is_empty()
508    }
509
510    pub fn has_string(&self) -> bool {
511        self.types.iter().any(TAtomic::is_string) && !self.types.is_empty()
512    }
513
514    pub fn is_float(&self) -> bool {
515        self.types.iter().all(TAtomic::is_float) && !self.types.is_empty()
516    }
517
518    pub fn is_bool(&self) -> bool {
519        self.types.iter().all(TAtomic::is_bool) && !self.types.is_empty()
520    }
521
522    pub fn is_never(&self) -> bool {
523        self.types.iter().all(TAtomic::is_never) || self.types.is_empty()
524    }
525
526    pub fn is_never_template(&self) -> bool {
527        self.types.iter().all(TAtomic::is_templated_as_never) && !self.types.is_empty()
528    }
529
530    #[must_use]
531    pub fn is_placeholder(&self) -> bool {
532        self.types.iter().all(|t| matches!(t, TAtomic::Placeholder)) && !self.types.is_empty()
533    }
534
535    /// Returns true if this union or any type parameter within it contains a Placeholder.
536    #[must_use]
537    pub fn contains_placeholder(&self) -> bool {
538        self.types.iter().any(|t| t.contains_placeholder())
539    }
540
541    pub fn is_true(&self) -> bool {
542        self.types.iter().all(TAtomic::is_true) && !self.types.is_empty()
543    }
544
545    pub fn is_false(&self) -> bool {
546        self.types.iter().all(TAtomic::is_false) && !self.types.is_empty()
547    }
548
549    #[must_use]
550    pub fn is_nonnull(&self) -> bool {
551        self.types.len() == 1 && matches!(self.types[0], TAtomic::Mixed(mixed) if mixed.is_non_null())
552    }
553
554    pub fn is_numeric(&self) -> bool {
555        self.types.iter().all(TAtomic::is_numeric) && !self.types.is_empty()
556    }
557
558    pub fn is_int_or_float(&self) -> bool {
559        self.types.iter().all(TAtomic::is_int_or_float) && !self.types.is_empty()
560    }
561
562    /// Returns `Some(true)` if all types are effectively int, `Some(false)` if all are effectively float,
563    /// or `None` if mixed or neither. Handles unions like `1|2` (all int) or `3.4|4.5` (all float).
564    #[must_use]
565    pub fn effective_int_or_float(&self) -> Option<bool> {
566        let mut result: Option<bool> = None;
567        for atomic in self.types.as_ref() {
568            {
569                let is_int = atomic.effective_int_or_float()?;
570                if let Some(prev) = result {
571                    if prev != is_int {
572                        return None;
573                    }
574                } else {
575                    result = Some(is_int);
576                }
577            }
578        }
579
580        result
581    }
582
583    #[must_use]
584    pub fn is_mixed(&self) -> bool {
585        self.types.iter().all(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
586    }
587
588    pub fn is_mixed_template(&self) -> bool {
589        self.types.iter().all(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
590    }
591
592    #[must_use]
593    pub fn has_mixed(&self) -> bool {
594        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
595    }
596
597    pub fn has_mixed_template(&self) -> bool {
598        self.types.iter().any(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
599    }
600
601    #[must_use]
602    pub fn has_nullable_mixed(&self) -> bool {
603        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(mixed) if !mixed.is_non_null())) && !self.types.is_empty()
604    }
605
606    #[must_use]
607    pub fn has_void(&self) -> bool {
608        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
609    }
610
611    #[must_use]
612    pub fn has_null(&self) -> bool {
613        self.types.iter().any(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
614    }
615
616    #[must_use]
617    pub fn has_nullish(&self) -> bool {
618        self.types.iter().any(|t| match t {
619            TAtomic::Null | TAtomic::Void => true,
620            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
621            TAtomic::GenericParameter(parameter) => parameter.constraint.has_nullish(),
622            _ => false,
623        }) && !self.types.is_empty()
624    }
625
626    #[must_use]
627    pub fn is_nullable_mixed(&self) -> bool {
628        if self.types.len() != 1 {
629            return false;
630        }
631
632        match &self.types[0] {
633            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
634            _ => false,
635        }
636    }
637
638    #[must_use]
639    pub fn is_falsy_mixed(&self) -> bool {
640        if self.types.len() != 1 {
641            return false;
642        }
643
644        matches!(&self.types[0], &TAtomic::Mixed(mixed) if mixed.is_falsy())
645    }
646
647    #[must_use]
648    pub fn is_vanilla_mixed(&self) -> bool {
649        if self.types.len() != 1 {
650            return false;
651        }
652
653        self.types[0].is_vanilla_mixed()
654    }
655
656    #[must_use]
657    pub fn is_templated_as_vanilla_mixed(&self) -> bool {
658        if self.types.len() != 1 {
659            return false;
660        }
661
662        self.types[0].is_templated_as_vanilla_mixed()
663    }
664
665    #[must_use]
666    pub fn has_template_or_static(&self) -> bool {
667        for atomic in self.types.as_ref() {
668            if let TAtomic::GenericParameter(_) = atomic {
669                return true;
670            }
671
672            if let TAtomic::Object(TObject::Named(named_object)) = atomic {
673                if named_object.is_static {
674                    return true;
675                }
676
677                if let Some(intersections) = named_object.get_intersection_types() {
678                    for intersection in intersections {
679                        if let TAtomic::GenericParameter(_) = intersection {
680                            return true;
681                        }
682                    }
683                }
684            }
685        }
686
687        false
688    }
689
690    #[must_use]
691    pub fn has_template(&self) -> bool {
692        for atomic in self.types.as_ref() {
693            if let TAtomic::GenericParameter(_) = atomic {
694                return true;
695            }
696
697            if let Some(intersections) = atomic.get_intersection_types() {
698                for intersection in intersections {
699                    if let TAtomic::GenericParameter(_) = intersection {
700                        return true;
701                    }
702                }
703            }
704        }
705
706        false
707    }
708
709    #[must_use]
710    pub fn has_template_types(&self) -> bool {
711        let all_child_nodes = self.get_all_child_nodes();
712
713        for child_node in all_child_nodes {
714            if let TypeRef::Atomic(
715                TAtomic::GenericParameter(_)
716                | TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })),
717            ) = child_node
718            {
719                return true;
720            }
721        }
722
723        false
724    }
725
726    #[must_use]
727    pub fn get_template_types(&self) -> Vec<&TAtomic> {
728        let all_child_nodes = self.get_all_child_nodes();
729
730        let mut template_types = Vec::new();
731
732        for child_node in all_child_nodes {
733            if let TypeRef::Atomic(inner) = child_node {
734                match inner {
735                    TAtomic::GenericParameter(_) => {
736                        template_types.push(inner);
737                    }
738                    TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })) => {
739                        template_types.push(inner);
740                    }
741                    _ => {}
742                }
743            }
744        }
745
746        template_types
747    }
748
749    pub fn is_objecty(&self) -> bool {
750        for atomic in self.types.as_ref() {
751            if let &TAtomic::Object(_) = atomic {
752                continue;
753            }
754
755            if let TAtomic::Callable(callable) = atomic
756                && callable.get_signature().is_none_or(super::atomic::callable::TCallableSignature::is_closure)
757            {
758                continue;
759            }
760
761            return false;
762        }
763
764        true
765    }
766
767    #[must_use]
768    pub fn is_generator(&self) -> bool {
769        for atomic in self.types.as_ref() {
770            if atomic.is_generator() {
771                continue;
772            }
773
774            return false;
775        }
776
777        true
778    }
779
780    #[must_use]
781    pub fn extends_or_implements(&self, codebase: &CodebaseMetadata, interface: &[u8]) -> bool {
782        for atomic in self.types.as_ref() {
783            if !atomic.extends_or_implements(codebase, interface) {
784                return false;
785            }
786        }
787
788        true
789    }
790
791    #[must_use]
792    pub fn is_generic_parameter(&self) -> bool {
793        self.types.len() == 1 && matches!(self.types[0], TAtomic::GenericParameter(_))
794    }
795
796    #[must_use]
797    pub fn get_generic_parameter_constraint(&self) -> Option<&TUnion> {
798        if self.is_generic_parameter()
799            && let TAtomic::GenericParameter(parameter) = &self.types[0]
800        {
801            return Some(&parameter.constraint);
802        }
803
804        None
805    }
806
807    #[must_use]
808    pub fn is_null(&self) -> bool {
809        self.types.iter().all(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
810    }
811
812    #[must_use]
813    pub fn is_nullable(&self) -> bool {
814        self.types.iter().any(|t| match t {
815            TAtomic::Null => self.types.len() >= 2,
816            TAtomic::GenericParameter(param) => param.constraint.is_nullable(),
817            _ => false,
818        })
819    }
820
821    #[must_use]
822    pub fn can_be_null(&self) -> bool {
823        self.types.iter().any(|t| match t {
824            TAtomic::Null => true,
825            TAtomic::Void => true,
826            TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
827            TAtomic::GenericParameter(param) => param.constraint.can_be_null(),
828            _ => false,
829        })
830    }
831
832    #[must_use]
833    pub fn is_void(&self) -> bool {
834        self.types.iter().all(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
835    }
836
837    #[must_use]
838    pub fn is_voidable(&self) -> bool {
839        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
840    }
841
842    pub fn has_resource(&self) -> bool {
843        self.types.iter().any(TAtomic::is_resource)
844    }
845
846    pub fn is_resource(&self) -> bool {
847        self.types.iter().all(TAtomic::is_resource) && !self.types.is_empty()
848    }
849
850    pub fn is_array(&self) -> bool {
851        self.types.iter().all(TAtomic::is_array) && !self.types.is_empty()
852    }
853
854    pub fn is_list(&self) -> bool {
855        self.types.iter().all(TAtomic::is_list) && !self.types.is_empty()
856    }
857
858    pub fn is_vanilla_array(&self) -> bool {
859        self.types.iter().all(TAtomic::is_vanilla_array) && !self.types.is_empty()
860    }
861
862    pub fn is_keyed_array(&self) -> bool {
863        self.types.iter().all(TAtomic::is_keyed_array) && !self.types.is_empty()
864    }
865
866    pub fn is_falsable(&self) -> bool {
867        self.types.len() >= 2 && self.types.iter().any(TAtomic::is_false)
868    }
869
870    #[must_use]
871    pub fn has_bool(&self) -> bool {
872        self.types.iter().any(|t| t.is_bool() || t.is_generic_scalar()) && !self.types.is_empty()
873    }
874
875    /// Checks if the union explicitly contains the generic `scalar` type.
876    ///
877    /// This is a specific check for the `scalar` type itself, not for a
878    /// combination of types that would form a scalar (e.g., `int|string|bool|float`).
879    /// For that, see `has_scalar_combination`.
880    pub fn has_scalar(&self) -> bool {
881        self.types.iter().any(TAtomic::is_generic_scalar)
882    }
883
884    /// Checks if the union contains a combination of types that is equivalent
885    /// to the generic `scalar` type (i.e., contains `int`, `float`, `bool`, and `string`).
886    #[must_use]
887    pub fn has_scalar_combination(&self) -> bool {
888        const HAS_INT: u8 = 1 << 0;
889        const HAS_FLOAT: u8 = 1 << 1;
890        const HAS_BOOL: u8 = 1 << 2;
891        const HAS_STRING: u8 = 1 << 3;
892        const ALL_SCALARS: u8 = HAS_INT | HAS_FLOAT | HAS_BOOL | HAS_STRING;
893
894        let mut flags = 0u8;
895
896        for atomic in self.types.as_ref() {
897            if atomic.is_int() {
898                flags |= HAS_INT;
899            } else if atomic.is_float() {
900                flags |= HAS_FLOAT;
901            } else if atomic.is_bool() {
902                flags |= HAS_BOOL;
903            } else if atomic.is_string() {
904                flags |= HAS_STRING;
905            } else if atomic.is_array_key() {
906                flags |= HAS_INT | HAS_STRING;
907            } else if atomic.is_numeric() {
908                // We don't add `string` as `numeric-string` does not contain `string` type
909                flags |= HAS_INT | HAS_FLOAT;
910            } else if atomic.is_generic_scalar() {
911                return true;
912            }
913
914            // Early exit if we've already found all scalar types
915            if flags == ALL_SCALARS {
916                return true;
917            }
918        }
919
920        flags == ALL_SCALARS
921    }
922    pub fn has_array_key(&self) -> bool {
923        self.types.iter().any(TAtomic::is_array_key)
924    }
925
926    pub fn has_iterable(&self) -> bool {
927        self.types.iter().any(TAtomic::is_iterable) && !self.types.is_empty()
928    }
929
930    pub fn has_array(&self) -> bool {
931        self.types.iter().any(TAtomic::is_array) && !self.types.is_empty()
932    }
933
934    #[must_use]
935    pub fn has_traversable(&self, codebase: &CodebaseMetadata) -> bool {
936        self.types.iter().any(|atomic| atomic.is_traversable(codebase)) && !self.types.is_empty()
937    }
938
939    #[must_use]
940    pub fn has_array_key_like(&self) -> bool {
941        self.types.iter().any(|atomic| atomic.is_array_key() || atomic.is_int() || atomic.is_string())
942    }
943
944    pub fn has_numeric(&self) -> bool {
945        self.types.iter().any(TAtomic::is_numeric) && !self.types.is_empty()
946    }
947
948    pub fn is_always_truthy(&self) -> bool {
949        self.types.iter().all(TAtomic::is_truthy) && !self.types.is_empty()
950    }
951
952    pub fn is_always_falsy(&self) -> bool {
953        self.types.iter().all(TAtomic::is_falsy) && !self.types.is_empty()
954    }
955
956    #[must_use]
957    pub fn is_literal_of(&self, other: &TUnion) -> bool {
958        let Some(other_atomic_type) = other.types.first() else {
959            return false;
960        };
961
962        match other_atomic_type {
963            TAtomic::Scalar(TScalar::String(_)) => {
964                for self_atomic_type in self.types.as_ref() {
965                    if self_atomic_type.is_string_of_literal_origin() {
966                        continue;
967                    }
968
969                    return false;
970                }
971
972                true
973            }
974            TAtomic::Scalar(TScalar::Integer(_)) => {
975                for self_atomic_type in self.types.as_ref() {
976                    if self_atomic_type.is_literal_int() {
977                        continue;
978                    }
979
980                    return false;
981                }
982
983                true
984            }
985            TAtomic::Scalar(TScalar::Float(_)) => {
986                for self_atomic_type in self.types.as_ref() {
987                    if self_atomic_type.is_literal_float() {
988                        continue;
989                    }
990
991                    return false;
992                }
993
994                true
995            }
996            _ => false,
997        }
998    }
999
1000    #[must_use]
1001    pub fn all_literals(&self) -> bool {
1002        self.types
1003            .iter()
1004            .all(|atomic| atomic.is_string_of_literal_origin() || atomic.is_literal_int() || atomic.is_literal_float())
1005    }
1006
1007    #[must_use]
1008    pub fn has_static_object(&self) -> bool {
1009        self.types
1010            .iter()
1011            .any(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_static))
1012    }
1013
1014    #[must_use]
1015    pub fn is_static_object(&self) -> bool {
1016        self.types
1017            .iter()
1018            .all(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_static))
1019    }
1020
1021    #[inline]
1022    #[must_use]
1023    pub fn is_single(&self) -> bool {
1024        self.types.len() == 1
1025    }
1026
1027    #[inline]
1028    #[must_use]
1029    pub fn get_single_string(&self) -> Option<&TString> {
1030        if self.is_single()
1031            && let TAtomic::Scalar(TScalar::String(string)) = &self.types[0]
1032        {
1033            Some(string)
1034        } else {
1035            None
1036        }
1037    }
1038
1039    #[inline]
1040    #[must_use]
1041    pub fn get_single_array(&self) -> Option<&TArray> {
1042        if self.is_single()
1043            && let TAtomic::Array(array) = &self.types[0]
1044        {
1045            Some(array)
1046        } else {
1047            None
1048        }
1049    }
1050
1051    #[inline]
1052    #[must_use]
1053    pub fn get_single_bool(&self) -> Option<&TBool> {
1054        if self.is_single()
1055            && let TAtomic::Scalar(TScalar::Bool(bool)) = &self.types[0]
1056        {
1057            Some(bool)
1058        } else {
1059            None
1060        }
1061    }
1062
1063    #[inline]
1064    #[must_use]
1065    pub fn get_single_named_object(&self) -> Option<&TNamedObject> {
1066        if self.is_single()
1067            && let TAtomic::Object(TObject::Named(named_object)) = &self.types[0]
1068        {
1069            Some(named_object)
1070        } else {
1071            None
1072        }
1073    }
1074
1075    #[inline]
1076    #[must_use]
1077    pub fn get_single_shaped_object(&self) -> Option<&TObjectWithProperties> {
1078        if self.is_single()
1079            && let TAtomic::Object(TObject::WithProperties(shaped_object)) = &self.types[0]
1080        {
1081            Some(shaped_object)
1082        } else {
1083            None
1084        }
1085    }
1086
1087    #[inline]
1088    #[must_use]
1089    pub fn get_single(&self) -> &TAtomic {
1090        &self.types[0]
1091    }
1092
1093    #[inline]
1094    #[must_use]
1095    pub fn get_single_owned(self) -> TAtomic {
1096        self.types[0].clone()
1097    }
1098
1099    #[inline]
1100    #[must_use]
1101    pub fn is_named_object(&self) -> bool {
1102        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1103    }
1104
1105    #[must_use]
1106    pub fn is_enum(&self) -> bool {
1107        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_))))
1108    }
1109
1110    #[must_use]
1111    pub fn is_enum_case(&self) -> bool {
1112        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1113    }
1114
1115    #[must_use]
1116    pub fn is_single_enum_case(&self) -> bool {
1117        self.is_single()
1118            && self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1119    }
1120
1121    #[inline]
1122    #[must_use]
1123    pub fn has_named_object(&self) -> bool {
1124        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1125    }
1126
1127    #[inline]
1128    #[must_use]
1129    pub fn has_object(&self) -> bool {
1130        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Any | TObject::WithProperties(_))))
1131    }
1132
1133    #[inline]
1134    #[must_use]
1135    pub fn has_callable(&self) -> bool {
1136        self.types.iter().any(|t| matches!(t, TAtomic::Callable(_)))
1137    }
1138
1139    #[inline]
1140    #[must_use]
1141    pub fn is_callable(&self) -> bool {
1142        self.types.iter().all(|t| matches!(t, TAtomic::Callable(_)))
1143    }
1144
1145    #[inline]
1146    #[must_use]
1147    pub fn has_object_type(&self) -> bool {
1148        self.types.iter().any(|t| matches!(t, TAtomic::Object(_)))
1149    }
1150
1151    /// Return a vector of pairs containing the enum name, and their case name
1152    /// if specified.
1153    #[must_use]
1154    pub fn get_enum_cases(&self) -> Vec<(Word, Option<Word>)> {
1155        self.types
1156            .iter()
1157            .filter_map(|t| match t {
1158                TAtomic::Object(TObject::Enum(enum_object)) => Some((enum_object.name, enum_object.case)),
1159                _ => None,
1160            })
1161            .collect()
1162    }
1163
1164    #[must_use]
1165    pub fn get_single_int(&self) -> Option<TInteger> {
1166        if self.is_single() { self.get_single().get_integer() } else { None }
1167    }
1168
1169    #[must_use]
1170    pub fn get_single_literal_int_value(&self) -> Option<i64> {
1171        if self.is_single() { self.get_single().get_literal_int_value() } else { None }
1172    }
1173
1174    #[must_use]
1175    pub fn get_single_maximum_int_value(&self) -> Option<i64> {
1176        if self.is_single() { self.get_single().get_maximum_int_value() } else { None }
1177    }
1178
1179    #[must_use]
1180    pub fn get_single_minimum_int_value(&self) -> Option<i64> {
1181        if self.is_single() { self.get_single().get_minimum_int_value() } else { None }
1182    }
1183
1184    /// Returns the maximum possible integer value across all types in this union.
1185    ///
1186    /// Returns `None` when the union is empty, contains a non-integer type, or contains
1187    /// an integer type without a known upper bound.
1188    #[must_use]
1189    pub fn get_maximum_int_value(&self) -> Option<i64> {
1190        let mut types = self.types.iter();
1191        let mut maximum = types.next()?.get_maximum_int_value()?;
1192
1193        for atomic in types {
1194            maximum = maximum.max(atomic.get_maximum_int_value()?);
1195        }
1196
1197        Some(maximum)
1198    }
1199
1200    /// Returns the minimum possible integer value across all types in this union.
1201    ///
1202    /// Returns `None` when the union is empty, contains a non-integer type, or contains
1203    /// an integer type without a known lower bound.
1204    #[must_use]
1205    pub fn get_minimum_int_value(&self) -> Option<i64> {
1206        let mut types = self.types.iter();
1207        let mut minimum = types.next()?.get_minimum_int_value()?;
1208
1209        for atomic in types {
1210            minimum = minimum.min(atomic.get_minimum_int_value()?);
1211        }
1212
1213        Some(minimum)
1214    }
1215
1216    #[must_use]
1217    pub fn get_single_literal_float_value(&self) -> Option<f64> {
1218        if self.is_single() { self.get_single().get_literal_float_value() } else { None }
1219    }
1220
1221    #[must_use]
1222    pub fn get_single_literal_string_value(&self) -> Option<&[u8]> {
1223        if self.is_single() { self.get_single().get_literal_string_value() } else { None }
1224    }
1225
1226    #[must_use]
1227    pub fn get_single_class_string_value(&self) -> Option<Word> {
1228        if self.is_single() { self.get_single().get_class_string_value() } else { None }
1229    }
1230
1231    #[must_use]
1232    pub fn get_single_array_key(&self) -> Option<ArrayKey> {
1233        if self.is_single() { self.get_single().to_array_key() } else { None }
1234    }
1235
1236    #[must_use]
1237    pub fn get_single_key_of_array_like(&self) -> Option<TUnion> {
1238        if !self.is_single() {
1239            return None;
1240        }
1241
1242        match self.get_single() {
1243            TAtomic::Array(array) => match array {
1244                TArray::List(_) => Some(get_int()),
1245                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1246                    Some((k, _)) => Some((**k).clone()),
1247                    None => Some(get_arraykey()),
1248                },
1249            },
1250            _ => None,
1251        }
1252    }
1253
1254    #[must_use]
1255    pub fn get_single_value_of_array_like(&self) -> Option<Cow<'_, TUnion>> {
1256        if !self.is_single() {
1257            return None;
1258        }
1259
1260        match self.get_single() {
1261            TAtomic::Array(array) => match array {
1262                TArray::List(list) => Some(Cow::Borrowed(&list.element_type)),
1263                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1264                    Some((_, v)) => Some(Cow::Borrowed(v)),
1265                    None => Some(Cow::Owned(get_mixed())),
1266                },
1267            },
1268            _ => None,
1269        }
1270    }
1271
1272    #[must_use]
1273    pub fn get_literal_ints(&self) -> Vec<&TAtomic> {
1274        self.types.iter().filter(|a| a.is_literal_int()).collect()
1275    }
1276
1277    #[must_use]
1278    pub fn get_literal_strings(&self) -> Vec<&TAtomic> {
1279        self.types.iter().filter(|a| a.is_known_literal_string()).collect()
1280    }
1281
1282    #[must_use]
1283    pub fn get_literal_string_values(&self) -> Vec<Option<Word>> {
1284        self.get_literal_strings()
1285            .into_iter()
1286            .map(|atom| match atom {
1287                TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(value)), .. })) => {
1288                    Some(*value)
1289                }
1290                _ => None,
1291            })
1292            .collect()
1293    }
1294
1295    #[must_use]
1296    pub fn has_literal_float(&self) -> bool {
1297        self.types.iter().any(|atomic| match atomic {
1298            TAtomic::Scalar(scalar) => scalar.is_literal_float(),
1299            _ => false,
1300        })
1301    }
1302
1303    #[must_use]
1304    pub fn has_literal_int(&self) -> bool {
1305        self.types.iter().any(|atomic| match atomic {
1306            TAtomic::Scalar(scalar) => scalar.is_literal_int(),
1307            _ => false,
1308        })
1309    }
1310
1311    #[must_use]
1312    pub fn has_literal_string(&self) -> bool {
1313        self.types.iter().any(|atomic| match atomic {
1314            TAtomic::Scalar(scalar) => scalar.is_known_literal_string(),
1315            _ => false,
1316        })
1317    }
1318
1319    #[must_use]
1320    pub fn has_literal_value(&self) -> bool {
1321        self.types.iter().any(|atomic| match atomic {
1322            TAtomic::Scalar(scalar) => scalar.is_literal_value(),
1323            _ => false,
1324        })
1325    }
1326
1327    #[must_use]
1328    pub fn accepts_false(&self) -> bool {
1329        self.types.iter().any(|t| match t {
1330            TAtomic::GenericParameter(parameter) => parameter.constraint.accepts_false(),
1331            TAtomic::Mixed(mixed) if !mixed.is_truthy() => true,
1332            TAtomic::Scalar(TScalar::Generic | TScalar::Bool(TBool { value: None | Some(false) })) => true,
1333            _ => false,
1334        })
1335    }
1336
1337    #[must_use]
1338    pub fn accepts_null(&self) -> bool {
1339        self.types.iter().any(|t| match t {
1340            TAtomic::GenericParameter(generic_parameter) => generic_parameter.constraint.accepts_null(),
1341            TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
1342            TAtomic::Null | TAtomic::Placeholder => true,
1343            _ => false,
1344        })
1345    }
1346}
1347
1348impl TType for TUnion {
1349    fn get_child_nodes(&self) -> Vec<TypeRef<'_>> {
1350        self.types.iter().map(TypeRef::Atomic).collect()
1351    }
1352
1353    fn needs_population(&self) -> bool {
1354        !self.flags.contains(UnionFlags::POPULATED) && self.types.iter().any(super::TType::needs_population)
1355    }
1356
1357    #[inline]
1358    fn is_expandable(&self) -> bool {
1359        if self.types.is_empty() {
1360            return true;
1361        }
1362
1363        self.types.iter().any(super::TType::is_expandable)
1364    }
1365
1366    fn is_complex(&self) -> bool {
1367        self.types.len() > 3 || self.types.iter().any(super::TType::is_complex)
1368    }
1369
1370    fn get_id(&self) -> Word {
1371        let len = self.types.len();
1372
1373        let mut atomic_ids: Vec<Word> = self
1374            .types
1375            .as_ref()
1376            .iter()
1377            .map(|atomic| {
1378                let id = atomic.get_id();
1379                if atomic.is_generic_parameter() || atomic.has_intersection_types() && len > 1 {
1380                    concat_word!(b"(", id.as_bytes(), b")")
1381                } else {
1382                    id
1383                }
1384            })
1385            .collect();
1386
1387        if len <= 1 {
1388            return atomic_ids.pop().unwrap_or_else(empty_word);
1389        }
1390
1391        atomic_ids.sort_unstable();
1392        join_words(&atomic_ids, b"|")
1393    }
1394
1395    fn get_pretty_id_with_indent(&self, indent: usize) -> Word {
1396        let len = self.types.len();
1397
1398        if len <= 1 {
1399            return self.types.first().map_or_else(empty_word, |atomic| atomic.get_pretty_id_with_indent(indent));
1400        }
1401
1402        // Use multiline format for unions with more than 3 types
1403        if len > 3 {
1404            let mut atomic_ids: Vec<Word> = self
1405                .types
1406                .as_ref()
1407                .iter()
1408                .map(|atomic| {
1409                    let id = atomic.get_pretty_id_with_indent(indent + 2);
1410                    if atomic.has_intersection_types() { concat_word!(b"(", id.as_bytes(), b")") } else { id }
1411                })
1412                .collect();
1413
1414            atomic_ids.sort_unstable();
1415
1416            let mut result: Vec<u8> = Vec::new();
1417            result.extend_from_slice(atomic_ids[0].as_bytes());
1418            for id in &atomic_ids[1..] {
1419                result.extend_from_slice(b"\n");
1420                result.resize(result.len() + indent, b' ');
1421                result.extend_from_slice(b"| ");
1422                result.extend_from_slice(id.as_bytes());
1423            }
1424
1425            word(&result)
1426        } else {
1427            // Use inline format for smaller unions
1428            let mut atomic_ids: Vec<Word> = self
1429                .types
1430                .as_ref()
1431                .iter()
1432                .map(|atomic| {
1433                    let id = atomic.get_pretty_id_with_indent(indent);
1434                    if atomic.has_intersection_types() && len > 1 {
1435                        concat_word!(b"(", id.as_bytes(), b")")
1436                    } else {
1437                        id
1438                    }
1439                })
1440                .collect();
1441
1442            atomic_ids.sort_unstable();
1443            let mut result = atomic_ids[0];
1444            for id in &atomic_ids[1..] {
1445                result = concat_word!(result.as_bytes(), b" | ", id.as_bytes());
1446            }
1447
1448            result
1449        }
1450    }
1451}
1452
1453impl PartialEq for TUnion {
1454    fn eq(&self, other: &TUnion) -> bool {
1455        if std::ptr::eq(self, other) {
1456            return true;
1457        }
1458
1459        const SEMANTIC_FLAGS: UnionFlags = UnionFlags::HAD_TEMPLATE
1460            .union(UnionFlags::BY_REFERENCE)
1461            .union(UnionFlags::REFERENCE_FREE)
1462            .union(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
1463            .union(UnionFlags::POSSIBLY_UNDEFINED)
1464            .union(UnionFlags::IGNORE_NULLABLE_ISSUES)
1465            .union(UnionFlags::IGNORE_FALSABLE_ISSUES)
1466            .union(UnionFlags::FROM_TEMPLATE_DEFAULT);
1467
1468        if self.flags.intersection(SEMANTIC_FLAGS) != other.flags.intersection(SEMANTIC_FLAGS) {
1469            return false;
1470        }
1471
1472        let len = self.types.len();
1473        if len != other.types.len() {
1474            return false;
1475        }
1476
1477        // Fast path: unions are commonly constructed in stable type order.
1478        // When order already matches, this keeps comparison linear.
1479        if self.types == other.types {
1480            return true;
1481        }
1482
1483        // Check self ⊆ other
1484        for i in 0..len {
1485            let mut has_match = false;
1486            for j in 0..len {
1487                if self.types[i] == other.types[j] {
1488                    has_match = true;
1489                    break;
1490                }
1491            }
1492
1493            if !has_match {
1494                return false;
1495            }
1496        }
1497
1498        // Check other ⊆ self (needed when duplicates exist in either side)
1499        for i in 0..len {
1500            let mut has_match = false;
1501            for j in 0..len {
1502                if other.types[i] == self.types[j] {
1503                    has_match = true;
1504                    break;
1505                }
1506            }
1507
1508            if !has_match {
1509                return false;
1510            }
1511        }
1512
1513        true
1514    }
1515}
1516
1517/// Coerces an atomic Vec to be non-empty by inserting `Never` when empty.
1518#[inline]
1519fn ensure_non_empty_types(mut types: Vec<TAtomic>) -> Vec<TAtomic> {
1520    if types.is_empty() {
1521        types.push(TAtomic::Never);
1522    }
1523
1524    types
1525}
1526
1527pub fn populate_union_type(
1528    unpopulated_union: &mut TUnion,
1529    codebase_symbols: &Symbols,
1530    reference_source: Option<&ReferenceSource>,
1531    symbol_references: &mut SymbolReferences,
1532    force: bool,
1533) {
1534    if unpopulated_union.flags.contains(UnionFlags::POPULATED) && !force {
1535        return;
1536    }
1537
1538    if !unpopulated_union.needs_population() {
1539        return;
1540    }
1541
1542    unpopulated_union.flags.insert(UnionFlags::POPULATED);
1543    let unpopulated_atomics = unpopulated_union.types.to_mut();
1544    for unpopulated_atomic in unpopulated_atomics {
1545        match unpopulated_atomic {
1546            TAtomic::Scalar(TScalar::ClassLikeString(
1547                TClassLikeString::Generic { constraint, .. } | TClassLikeString::OfType { constraint, .. },
1548            )) => {
1549                populate_atomic_type(
1550                    Arc::make_mut(constraint),
1551                    codebase_symbols,
1552                    reference_source,
1553                    symbol_references,
1554                    force,
1555                );
1556            }
1557            _ => {
1558                populate_atomic_type(unpopulated_atomic, codebase_symbols, reference_source, symbol_references, force);
1559            }
1560        }
1561    }
1562}
1563
1564/// Recursively generalises every narrowed scalar (string, int, float, bool) to
1565/// its general form within the given atomic type.  Descends into every
1566/// nested `TUnion` so a narrowing buried in an array element, object type
1567/// parameter, generic constraint, conditional branch, etc., is also widened.
1568fn widen_atomic_scalars(atomic: &mut TAtomic) {
1569    match atomic {
1570        TAtomic::Scalar(scalar) => widen_scalar(scalar),
1571        TAtomic::Array(array) => match array {
1572            TArray::List(list) => {
1573                widen_arc_union_scalars(&mut list.element_type);
1574                if let Some(known) = list.known_elements.as_mut() {
1575                    for (_, ty) in known.values_mut() {
1576                        ty.widen_scalars();
1577                    }
1578                }
1579            }
1580            TArray::Keyed(keyed) => {
1581                if let Some((key, value)) = keyed.parameters.as_mut() {
1582                    widen_arc_union_scalars(key);
1583                    widen_arc_union_scalars(value);
1584                }
1585                if let Some(known) = keyed.known_items.as_mut() {
1586                    for (_, ty) in known.values_mut() {
1587                        ty.widen_scalars();
1588                    }
1589                }
1590            }
1591        },
1592        TAtomic::Iterable(iterable) => {
1593            widen_arc_union_scalars(&mut iterable.key_type);
1594            widen_arc_union_scalars(&mut iterable.value_type);
1595            if let Some(intersections) = iterable.intersection_types.as_mut() {
1596                for inner in intersections.iter_mut() {
1597                    widen_atomic_scalars(inner);
1598                }
1599            }
1600        }
1601        TAtomic::Object(TObject::Named(named)) => {
1602            if let Some(params) = named.type_parameters.as_mut() {
1603                for ty in params.iter_mut() {
1604                    ty.widen_scalars();
1605                }
1606            }
1607        }
1608        TAtomic::Object(TObject::WithProperties(with_props)) => {
1609            for (_, ty) in with_props.known_properties.values_mut() {
1610                ty.widen_scalars();
1611            }
1612        }
1613        TAtomic::GenericParameter(generic) => {
1614            widen_arc_union_scalars(&mut generic.constraint);
1615            if let Some(intersections) = generic.intersection_types.as_mut() {
1616                for inner in intersections.iter_mut() {
1617                    widen_atomic_scalars(inner);
1618                }
1619            }
1620        }
1621        TAtomic::Conditional(conditional) => {
1622            widen_arc_union_scalars(&mut conditional.subject);
1623            widen_arc_union_scalars(&mut conditional.target);
1624            widen_arc_union_scalars(&mut conditional.then);
1625            widen_arc_union_scalars(&mut conditional.otherwise);
1626        }
1627        _ => {}
1628    }
1629}
1630
1631#[inline]
1632fn widen_arc_union_scalars(union: &mut Arc<TUnion>) {
1633    if union_has_widenable_nested_scalar(union) {
1634        Arc::make_mut(union).widen_scalars();
1635    }
1636}
1637
1638fn widen_scalar(scalar: &mut TScalar) {
1639    match scalar {
1640        TScalar::String(string) if !is_string_fully_general(string) => {
1641            *string = TString::general();
1642        }
1643        TScalar::Integer(integer) if !matches!(integer, TInteger::Unspecified) => {
1644            *integer = TInteger::Unspecified;
1645        }
1646        TScalar::Float(float) if !matches!(float, TFloat::Float) => {
1647            *float = TFloat::Float;
1648        }
1649        TScalar::Bool(b) if !b.is_general() => {
1650            *b = TBool::general();
1651        }
1652        _ => {}
1653    }
1654}
1655
1656#[inline]
1657fn is_string_fully_general(string: &TString) -> bool {
1658    string.literal.is_none()
1659        && !string.is_numeric
1660        && !string.is_truthy
1661        && !string.is_non_empty
1662        && !string.is_callable
1663        && matches!(string.casing, TStringCasing::Unspecified)
1664}
1665
1666/// Like `widen_atomic_scalars`, but only widens *literal* scalars (e.g.
1667/// `int(42)`, `'foo'`, `true`/`false`) - preserves user-declared narrowings
1668/// such as `non-negative-int`, `non-empty-string`, or `int<1, max>`. Used when
1669/// the surrounding context (e.g. `@param-out` on a generic function) commits
1670/// to maintaining narrow types through the call.
1671fn widen_atomic_literals(atomic: &mut TAtomic) {
1672    match atomic {
1673        TAtomic::Scalar(scalar) => widen_scalar_literal(scalar),
1674        TAtomic::Array(array) => match array {
1675            TArray::List(list) => {
1676                widen_arc_union_literals(&mut list.element_type);
1677                if let Some(known) = list.known_elements.as_mut() {
1678                    for (_, ty) in known.values_mut() {
1679                        ty.widen_literals();
1680                    }
1681                }
1682            }
1683            TArray::Keyed(keyed) => {
1684                if let Some((key, value)) = keyed.parameters.as_mut() {
1685                    widen_arc_union_literals(key);
1686                    widen_arc_union_literals(value);
1687                }
1688                if let Some(known) = keyed.known_items.as_mut() {
1689                    for (_, ty) in known.values_mut() {
1690                        ty.widen_literals();
1691                    }
1692                }
1693            }
1694        },
1695        TAtomic::Iterable(iterable) => {
1696            widen_arc_union_literals(&mut iterable.key_type);
1697            widen_arc_union_literals(&mut iterable.value_type);
1698            if let Some(intersections) = iterable.intersection_types.as_mut() {
1699                for inner in intersections.iter_mut() {
1700                    widen_atomic_literals(inner);
1701                }
1702            }
1703        }
1704        TAtomic::Object(TObject::Named(named)) => {
1705            if let Some(params) = named.type_parameters.as_mut() {
1706                for ty in params.iter_mut() {
1707                    ty.widen_literals();
1708                }
1709            }
1710        }
1711        TAtomic::Object(TObject::WithProperties(with_props)) => {
1712            for (_, ty) in with_props.known_properties.values_mut() {
1713                ty.widen_literals();
1714            }
1715        }
1716        TAtomic::GenericParameter(generic) => {
1717            widen_arc_union_literals(&mut generic.constraint);
1718            if let Some(intersections) = generic.intersection_types.as_mut() {
1719                for inner in intersections.iter_mut() {
1720                    widen_atomic_literals(inner);
1721                }
1722            }
1723        }
1724        TAtomic::Conditional(conditional) => {
1725            widen_arc_union_literals(&mut conditional.subject);
1726            widen_arc_union_literals(&mut conditional.target);
1727            widen_arc_union_literals(&mut conditional.then);
1728            widen_arc_union_literals(&mut conditional.otherwise);
1729        }
1730        _ => {}
1731    }
1732}
1733
1734#[inline]
1735fn widen_arc_union_literals(union: &mut Arc<TUnion>) {
1736    if union_has_widenable_nested_literal(union) {
1737        Arc::make_mut(union).widen_literals();
1738    }
1739}
1740
1741fn widen_scalar_literal(scalar: &mut TScalar) {
1742    match scalar {
1743        TScalar::String(string) if string.literal.is_some() => {
1744            *string = string.without_literal();
1745        }
1746        TScalar::Integer(integer) if matches!(integer, TInteger::Literal(_) | TInteger::UnspecifiedLiteral) => {
1747            *integer = TInteger::Unspecified;
1748        }
1749        TScalar::Float(float) if matches!(float, TFloat::Literal(_) | TFloat::UnspecifiedLiteral) => {
1750            *float = TFloat::Float;
1751        }
1752        TScalar::Bool(b) if !b.is_general() => {
1753            *b = TBool::general();
1754        }
1755        _ => {}
1756    }
1757}
1758
1759fn union_has_widenable_nested_literal(union: &TUnion) -> bool {
1760    union.types.iter().any(atomic_has_widenable_literal)
1761}
1762
1763fn atomic_has_widenable_literal(atomic: &TAtomic) -> bool {
1764    match atomic {
1765        TAtomic::Scalar(TScalar::String(s)) => s.literal.is_some(),
1766        TAtomic::Scalar(TScalar::Integer(i)) => matches!(i, TInteger::Literal(_) | TInteger::UnspecifiedLiteral),
1767        TAtomic::Scalar(TScalar::Float(f)) => matches!(f, TFloat::Literal(_) | TFloat::UnspecifiedLiteral),
1768        TAtomic::Scalar(TScalar::Bool(b)) => !b.is_general(),
1769        TAtomic::Array(TArray::List(list)) => {
1770            union_has_widenable_nested_literal(&list.element_type)
1771                || list
1772                    .known_elements
1773                    .as_ref()
1774                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_literal(t)))
1775        }
1776        TAtomic::Array(TArray::Keyed(keyed)) => {
1777            keyed
1778                .parameters
1779                .as_ref()
1780                .is_some_and(|(k, v)| union_has_widenable_nested_literal(k) || union_has_widenable_nested_literal(v))
1781                || keyed
1782                    .known_items
1783                    .as_ref()
1784                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_literal(t)))
1785        }
1786        TAtomic::Iterable(iterable) => {
1787            union_has_widenable_nested_literal(&iterable.key_type)
1788                || union_has_widenable_nested_literal(&iterable.value_type)
1789                || iterable.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_literal))
1790        }
1791        TAtomic::Object(TObject::Named(named)) => {
1792            named.type_parameters.as_ref().is_some_and(|p| p.iter().any(union_has_widenable_nested_literal))
1793        }
1794        TAtomic::Object(TObject::WithProperties(with_props)) => {
1795            with_props.known_properties.values().any(|(_, t)| union_has_widenable_nested_literal(t))
1796        }
1797        TAtomic::GenericParameter(generic) => {
1798            union_has_widenable_nested_literal(&generic.constraint)
1799                || generic.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_literal))
1800        }
1801        TAtomic::Conditional(conditional) => {
1802            union_has_widenable_nested_literal(&conditional.subject)
1803                || union_has_widenable_nested_literal(&conditional.target)
1804                || union_has_widenable_nested_literal(&conditional.then)
1805                || union_has_widenable_nested_literal(&conditional.otherwise)
1806        }
1807        _ => false,
1808    }
1809}
1810
1811/// Returns `true` if any atom in the union holds (somewhere recursively) a
1812/// type that `widen_atomic_scalars` would mutate.  Used as a cheap pre-check
1813/// to avoid `Arc::make_mut` on shared unions that don't need widening.
1814fn union_has_widenable_nested_scalar(union: &TUnion) -> bool {
1815    union.types.iter().any(atomic_has_widenable_scalar)
1816}
1817
1818fn atomic_has_widenable_scalar(atomic: &TAtomic) -> bool {
1819    match atomic {
1820        TAtomic::Scalar(TScalar::String(s)) => !is_string_fully_general(s),
1821        TAtomic::Scalar(TScalar::Integer(i)) => !matches!(i, TInteger::Unspecified),
1822        TAtomic::Scalar(TScalar::Float(f)) => !matches!(f, TFloat::Float),
1823        TAtomic::Scalar(TScalar::Bool(b)) => !b.is_general(),
1824        TAtomic::Array(TArray::List(list)) => {
1825            union_has_widenable_nested_scalar(&list.element_type)
1826                || list
1827                    .known_elements
1828                    .as_ref()
1829                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_scalar(t)))
1830        }
1831        TAtomic::Array(TArray::Keyed(keyed)) => {
1832            keyed
1833                .parameters
1834                .as_ref()
1835                .is_some_and(|(k, v)| union_has_widenable_nested_scalar(k) || union_has_widenable_nested_scalar(v))
1836                || keyed
1837                    .known_items
1838                    .as_ref()
1839                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_scalar(t)))
1840        }
1841        TAtomic::Iterable(iterable) => {
1842            union_has_widenable_nested_scalar(&iterable.key_type)
1843                || union_has_widenable_nested_scalar(&iterable.value_type)
1844                || iterable.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_scalar))
1845        }
1846        TAtomic::Object(TObject::Named(named)) => {
1847            named.type_parameters.as_ref().is_some_and(|p| p.iter().any(union_has_widenable_nested_scalar))
1848        }
1849        TAtomic::Object(TObject::WithProperties(with_props)) => {
1850            with_props.known_properties.values().any(|(_, t)| union_has_widenable_nested_scalar(t))
1851        }
1852        TAtomic::GenericParameter(generic) => {
1853            union_has_widenable_nested_scalar(&generic.constraint)
1854                || generic.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_scalar))
1855        }
1856        TAtomic::Conditional(conditional) => {
1857            union_has_widenable_nested_scalar(&conditional.subject)
1858                || union_has_widenable_nested_scalar(&conditional.target)
1859                || union_has_widenable_nested_scalar(&conditional.then)
1860                || union_has_widenable_nested_scalar(&conditional.otherwise)
1861        }
1862        _ => false,
1863    }
1864}