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