Skip to main content

mago_codex/ttype/
combiner.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::sync::LazyLock;
4
5use foldhash::HashSet;
6
7use mago_word::Word;
8use mago_word::WordSet;
9use mago_word::word;
10
11static ATOM_FALSE: LazyLock<Word> = LazyLock::new(|| word("false"));
12static ATOM_TRUE: LazyLock<Word> = LazyLock::new(|| word("true"));
13static ATOM_BOOL: LazyLock<Word> = LazyLock::new(|| word("bool"));
14static ATOM_VOID: LazyLock<Word> = LazyLock::new(|| word("void"));
15static ATOM_NULL: LazyLock<Word> = LazyLock::new(|| word("null"));
16static ATOM_STRING: LazyLock<Word> = LazyLock::new(|| word("string"));
17static ATOM_FLOAT: LazyLock<Word> = LazyLock::new(|| word("float"));
18static ATOM_INT: LazyLock<Word> = LazyLock::new(|| word("int"));
19static ATOM_MIXED: LazyLock<Word> = LazyLock::new(|| word("mixed"));
20static ATOM_SCALAR: LazyLock<Word> = LazyLock::new(|| word("scalar"));
21static ATOM_ARRAY_KEY: LazyLock<Word> = LazyLock::new(|| word("array-key"));
22static ATOM_NUMERIC: LazyLock<Word> = LazyLock::new(|| word("numeric"));
23static ATOM_NEVER: LazyLock<Word> = LazyLock::new(|| word("never"));
24
25use crate::metadata::CodebaseMetadata;
26use crate::symbol::SymbolKind;
27use crate::ttype::TType;
28use crate::ttype::atomic::TAtomic;
29use crate::ttype::atomic::array::TArray;
30use crate::ttype::atomic::array::key::ArrayKey;
31use crate::ttype::atomic::array::keyed::TKeyedArray;
32use crate::ttype::atomic::array::list::TList;
33use crate::ttype::atomic::mixed::TMixed;
34use crate::ttype::atomic::mixed::truthiness::TMixedTruthiness;
35use crate::ttype::atomic::object::TObject;
36use crate::ttype::atomic::object::named::TNamedObject;
37use crate::ttype::atomic::resource::TResource;
38use crate::ttype::atomic::scalar::TScalar;
39use crate::ttype::atomic::scalar::float::TFloat;
40use crate::ttype::atomic::scalar::int::TInteger;
41use crate::ttype::atomic::scalar::string::TString;
42use crate::ttype::atomic::scalar::string::TStringCasing;
43use crate::ttype::atomic::scalar::string::TStringLiteral;
44use crate::ttype::combination::CombinationFlags;
45use crate::ttype::combination::TypeCombination;
46use crate::ttype::combine_union_types;
47use crate::ttype::comparator::ComparisonResult;
48use crate::ttype::comparator::array_comparator::is_array_contained_by_array;
49use crate::ttype::comparator::object_comparator;
50use crate::ttype::comparator::union_comparator;
51use crate::ttype::template::variance::Variance;
52use crate::ttype::union::TUnion;
53use crate::utils::str_is_numeric;
54
55/// Default maximum number of sealed arrays to track before generalizing.
56///
57/// When combining array types, sealed arrays (arrays with known literal elements)
58/// are accumulated for later comparison. If the number of sealed arrays exceeds
59/// this threshold, they are immediately generalized to prevent O(n²) complexity
60/// in `finalize_sealed_arrays` and excessive memory usage.
61pub const DEFAULT_ARRAY_COMBINATION_THRESHOLD: u16 = 32;
62
63/// Default maximum number of literal strings to track before generalizing to string.
64///
65/// When combining types with many different literal string values, tracking each
66/// literal individually causes O(n) memory and O(n²) comparison time.
67/// Once the threshold is exceeded, we generalize to the base string type.
68pub const DEFAULT_STRING_COMBINATION_THRESHOLD: u16 = 128;
69
70/// Default maximum number of literal integers to track before generalizing to int.
71///
72/// When combining types with many different literal integer values, tracking each
73/// literal individually causes O(n) memory and O(n²) comparison time.
74/// Once the threshold is exceeded, we generalize to the base int type.
75pub const DEFAULT_INTEGER_COMBINATION_THRESHOLD: u16 = 128;
76
77/// Options for controlling type combination behavior.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct CombinerOptions {
80    /// When true, empty arrays are overwritten by non-empty arrays during combination.
81    pub overwrite_empty_array: bool,
82    /// Maximum number of sealed arrays to track before generalizing.
83    pub array_combination_threshold: u16,
84    /// Maximum number of literal strings to track before generalizing to string.
85    pub string_combination_threshold: u16,
86    /// Maximum number of literal integers to track before generalizing to int.
87    pub integer_combination_threshold: u16,
88}
89
90impl Default for CombinerOptions {
91    fn default() -> Self {
92        Self {
93            overwrite_empty_array: false,
94            array_combination_threshold: DEFAULT_ARRAY_COMBINATION_THRESHOLD,
95            string_combination_threshold: DEFAULT_STRING_COMBINATION_THRESHOLD,
96            integer_combination_threshold: DEFAULT_INTEGER_COMBINATION_THRESHOLD,
97        }
98    }
99}
100
101impl CombinerOptions {
102    /// Create options with overwrite_empty_array set to true.
103    #[inline]
104    #[must_use]
105    pub fn with_overwrite_empty_array(mut self) -> Self {
106        self.overwrite_empty_array = true;
107        self
108    }
109
110    /// Create options with a custom array combination threshold.
111    #[inline]
112    #[must_use]
113    pub fn with_array_combination_threshold(mut self, threshold: u16) -> Self {
114        self.array_combination_threshold = threshold;
115        self
116    }
117
118    /// Create options with a custom string combination threshold.
119    #[inline]
120    #[must_use]
121    pub fn with_string_combination_threshold(mut self, threshold: u16) -> Self {
122        self.string_combination_threshold = threshold;
123        self
124    }
125
126    /// Create options with a custom integer combination threshold.
127    #[inline]
128    #[must_use]
129    pub fn with_integer_combination_threshold(mut self, threshold: u16) -> Self {
130        self.integer_combination_threshold = threshold;
131        self
132    }
133}
134
135pub fn combine_preserving_array_shapes(
136    types: Vec<TAtomic>,
137    codebase: &CodebaseMetadata,
138    options: CombinerOptions,
139) -> Vec<TAtomic> {
140    if types.iter().filter(|atomic| atomic.is_array()).count() < 2 {
141        return combine(types, codebase, options);
142    }
143
144    let (mut array_types, other_types): (Vec<_>, Vec<_>) = types.into_iter().partition(TAtomic::is_array);
145    let mut combined_types = if other_types.is_empty() { Vec::new() } else { combine(other_types, codebase, options) };
146
147    if combined_types.iter().any(|atomic| matches!(atomic, TAtomic::Mixed(mixed) if mixed.is_vanilla())) {
148        return combined_types;
149    }
150
151    combined_types.retain(|atomic| !atomic.is_never());
152    for atomic in &mut combined_types {
153        if matches!(atomic, TAtomic::Void) {
154            *atomic = TAtomic::Null;
155        }
156    }
157
158    combined_types.append(&mut array_types);
159    combined_types.sort_unstable();
160    combined_types.dedup();
161
162    combined_types
163}
164
165pub fn combine(types: Vec<TAtomic>, codebase: &CodebaseMetadata, options: CombinerOptions) -> Vec<TAtomic> {
166    if types.is_empty() {
167        debug_assert!(false, "combine() received an empty Vec; this is a caller bug");
168
169        return vec![TAtomic::Never];
170    }
171
172    if types.len() == 1 {
173        return types;
174    }
175
176    let mut combination = TypeCombination::new();
177
178    for atomic in types {
179        if let TAtomic::Derived(derived) = atomic {
180            combination.derived_types.insert(derived);
181            continue;
182        }
183
184        scrape_type_properties(atomic, &mut combination, codebase, options);
185    }
186
187    combination.integers.sort_unstable();
188    combination.integers.dedup();
189    combination.literal_floats.sort_unstable();
190    combination.literal_floats.dedup();
191
192    finalize_sealed_arrays(&mut combination.sealed_arrays, codebase);
193
194    let is_falsy_mixed = combination.flags.falsy_mixed().unwrap_or(false);
195    let is_truthy_mixed = combination.flags.truthy_mixed().unwrap_or(false);
196    let is_nonnull_mixed = combination.flags.nonnull_mixed().unwrap_or(false);
197
198    if is_falsy_mixed
199        || is_nonnull_mixed
200        || combination.flags.contains(CombinationFlags::GENERIC_MIXED)
201        || is_truthy_mixed
202    {
203        return vec![TAtomic::Mixed(TMixed::new().with_is_non_null(is_nonnull_mixed).with_truthiness(
204            if is_truthy_mixed && !is_falsy_mixed {
205                TMixedTruthiness::Truthy
206            } else if is_falsy_mixed && !is_truthy_mixed {
207                TMixedTruthiness::Falsy
208            } else {
209                TMixedTruthiness::Undetermined
210            },
211        ))];
212    } else if combination.flags.contains(CombinationFlags::HAS_MIXED) {
213        return vec![TAtomic::Mixed(TMixed::new())];
214    }
215
216    // `never` is the bottom type, absorbed by any other member of the union. Drop it up front so
217    // that it does not make the combination look non-simple, which would turn `void` into `null`.
218    if combination.value_types.len() > 1 {
219        combination.value_types.remove(&*ATOM_NEVER);
220    }
221
222    if combination.is_simple() {
223        if combination.value_types.contains_key(&*ATOM_FALSE) {
224            return vec![TAtomic::Scalar(TScalar::r#false())];
225        }
226
227        if combination.value_types.contains_key(&*ATOM_TRUE) {
228            return vec![TAtomic::Scalar(TScalar::r#true())];
229        }
230
231        return combination.value_types.into_values().collect();
232    }
233
234    if combination.value_types.remove(&*ATOM_VOID).is_some() {
235        combination.value_types.insert(*ATOM_NULL, TAtomic::Null);
236    }
237
238    if combination.value_types.contains_key(&*ATOM_FALSE) && combination.value_types.contains_key(&*ATOM_TRUE) {
239        combination.value_types.remove(&*ATOM_FALSE);
240        combination.value_types.remove(&*ATOM_TRUE);
241        combination.value_types.insert(*ATOM_BOOL, TAtomic::Scalar(TScalar::bool()));
242    }
243
244    let estimated_capacity = combination.derived_types.len()
245        + combination.integers.len().min(10)
246        + combination.literal_floats.len()
247        + combination.enum_names.len()
248        + combination.value_types.len()
249        + combination.sealed_arrays.len()
250        + 5;
251
252    let mut new_types = Vec::with_capacity(estimated_capacity);
253    for derived_type in combination.derived_types {
254        new_types.push(TAtomic::Derived(derived_type));
255    }
256
257    if combination.flags.contains(CombinationFlags::RESOURCE) {
258        new_types.push(TAtomic::Resource(TResource { closed: None }));
259    } else {
260        let open = combination.flags.contains(CombinationFlags::OPEN_RESOURCE);
261        let closed = combination.flags.contains(CombinationFlags::CLOSED_RESOURCE);
262        match (open, closed) {
263            (true, true) => {
264                new_types.push(TAtomic::Resource(TResource { closed: None }));
265            }
266            (true, false) => {
267                new_types.push(TAtomic::Resource(TResource { closed: Some(false) }));
268            }
269            (false, true) => {
270                new_types.push(TAtomic::Resource(TResource { closed: Some(true) }));
271            }
272            _ => {
273                // No resource type, do nothing
274            }
275        }
276    }
277
278    let mut arrays = vec![];
279
280    if combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY) {
281        arrays.push(TArray::Keyed(TKeyedArray {
282            known_items: if combination.keyed_array_entries.is_empty() {
283                None
284            } else {
285                Some(combination.keyed_array_entries)
286            },
287            parameters: if let Some((k, v)) = combination.keyed_array_parameters {
288                Some((Arc::new(k), Arc::new(v)))
289            } else {
290                None
291            },
292            non_empty: combination.flags.contains(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED),
293            known_non_list: combination.flags.contains(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST),
294        }));
295    }
296
297    if let Some(list_parameter) = combination.list_array_parameter {
298        arrays.push(TArray::List(TList {
299            known_elements: if combination.list_array_entries.is_empty() {
300                None
301            } else {
302                Some(combination.list_array_entries)
303            },
304            element_type: Arc::new(list_parameter),
305            non_empty: combination.flags.contains(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED),
306            known_count: None,
307        }));
308    }
309
310    for array in combination.sealed_arrays {
311        arrays.push(array);
312    }
313
314    if arrays.is_empty() && combination.flags.contains(CombinationFlags::HAS_EMPTY_ARRAY) {
315        arrays.push(TArray::Keyed(TKeyedArray {
316            known_items: None,
317            parameters: None,
318            non_empty: false,
319            known_non_list: false,
320        }));
321    }
322
323    new_types.extend(arrays.into_iter().map(TAtomic::Array));
324
325    for (_, (generic_type, generic_type_parameters)) in combination.object_type_params {
326        let generic_object = TAtomic::Object(TObject::Named(
327            TNamedObject::new(generic_type)
328                .with_is_static(*combination.object_static.get(&generic_type).unwrap_or(&false))
329                .with_type_parameters(Some(generic_type_parameters)),
330        ));
331
332        new_types.push(generic_object);
333    }
334
335    new_types.extend(combination.literal_strings.into_iter().map(|s| TAtomic::Scalar(TScalar::literal_string(s))));
336
337    if combination.value_types.contains_key(&*ATOM_STRING)
338        && combination.value_types.contains_key(&*ATOM_FLOAT)
339        && combination.value_types.contains_key(&*ATOM_BOOL)
340        && combination.integers.iter().any(super::atomic::scalar::int::TInteger::is_unspecified)
341    {
342        combination.integers.clear();
343        combination.value_types.remove(&*ATOM_STRING);
344        combination.value_types.remove(&*ATOM_FLOAT);
345        combination.value_types.remove(&*ATOM_BOOL);
346
347        new_types.push(TAtomic::Scalar(TScalar::Generic));
348    }
349
350    new_types.extend(TInteger::combine(combination.integers));
351    new_types.extend(combination.literal_floats.into_iter().map(|f| TAtomic::Scalar(TScalar::literal_float(f.into()))));
352
353    for (enum_name, enum_case) in combination.enum_names {
354        if combination.value_types.contains_key(&enum_name) {
355            continue;
356        }
357
358        let enum_object = match enum_case {
359            Some(case) => TAtomic::Object(TObject::new_enum_case(enum_name, case)),
360            None => TAtomic::Object(TObject::new_enum(enum_name)),
361        };
362
363        combination.value_types.insert(enum_object.get_id(), enum_object);
364    }
365
366    let mut has_never = combination.value_types.contains_key(&*ATOM_NEVER);
367
368    let combination_value_type_count = combination.value_types.len();
369    let mixed_from_loop_isset = combination.flags.mixed_from_loop_isset().unwrap_or(false);
370
371    for (_, atomic) in combination.value_types {
372        let tc = usize::from(has_never);
373        if atomic.is_mixed()
374            && mixed_from_loop_isset
375            && (combination_value_type_count > (tc + 1) || new_types.len() > tc)
376        {
377            continue;
378        }
379
380        if (atomic.is_never() || atomic.is_templated_as_never())
381            && (combination_value_type_count > 1 || !new_types.is_empty())
382        {
383            has_never = true;
384            continue;
385        }
386
387        new_types.push(atomic);
388    }
389
390    if new_types.is_empty() {
391        debug_assert!(has_never, "combine(): empty result without a `never` atomic in the combination");
392
393        return vec![TAtomic::Never];
394    }
395
396    new_types
397}
398
399fn finalize_sealed_arrays(arrays: &mut Vec<TArray>, codebase: &CodebaseMetadata) {
400    if arrays.len() <= 1 {
401        return;
402    }
403
404    arrays.sort_unstable_by_key(|a| match a {
405        TArray::List(list) => list.known_elements.as_ref().map_or(0, std::collections::BTreeMap::len),
406        TArray::Keyed(keyed) => keyed.known_items.as_ref().map_or(0, std::collections::BTreeMap::len),
407    });
408
409    let mut keep = vec![true; arrays.len()];
410
411    for i in 0..arrays.len() {
412        if !keep[i] {
413            continue;
414        }
415
416        for j in (i + 1)..arrays.len() {
417            if !keep[j] {
418                continue;
419            }
420
421            if is_array_contained_by_array(codebase, &arrays[i], &arrays[j], false, &mut ComparisonResult::new()) {
422                keep[i] = false;
423                break;
424            }
425
426            if is_array_contained_by_array(codebase, &arrays[j], &arrays[i], false, &mut ComparisonResult::new()) {
427                keep[j] = false;
428            }
429        }
430    }
431
432    let mut write = 0;
433    for (read, item) in keep.iter().enumerate().take(arrays.len()) {
434        if *item {
435            if write != read {
436                arrays.swap(write, read);
437            }
438
439            write += 1;
440        }
441    }
442
443    arrays.truncate(write);
444}
445
446fn scrape_type_properties(
447    atomic: TAtomic,
448    combination: &mut TypeCombination,
449    codebase: &CodebaseMetadata,
450    options: CombinerOptions,
451) {
452    if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
453        return;
454    }
455
456    if let TAtomic::Mixed(mixed) = atomic {
457        if mixed.is_isset_from_loop() {
458            if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
459                return; // Exit early, existing state is sufficient or broader
460            }
461
462            if combination.flags.mixed_from_loop_isset().is_none() {
463                combination.flags.set_mixed_from_loop_isset(Some(true));
464            }
465
466            combination.value_types.insert(*ATOM_MIXED, atomic);
467
468            return;
469        }
470
471        combination.flags.insert(CombinationFlags::HAS_MIXED);
472
473        if mixed.is_vanilla() {
474            combination.flags.set_falsy_mixed(Some(false));
475            combination.flags.set_truthy_mixed(Some(false));
476            combination.flags.set_mixed_from_loop_isset(Some(false));
477            combination.flags.insert(CombinationFlags::GENERIC_MIXED);
478
479            return;
480        }
481
482        if mixed.is_truthy() {
483            if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
484                return;
485            }
486
487            combination.flags.set_mixed_from_loop_isset(Some(false));
488
489            if combination.flags.falsy_mixed().unwrap_or(false) {
490                combination.flags.insert(CombinationFlags::GENERIC_MIXED);
491                combination.flags.set_falsy_mixed(Some(false));
492                return;
493            }
494
495            if combination.flags.truthy_mixed().is_some() {
496                return;
497            }
498
499            let has_non_truthy = combination.value_types.values().any(|v| !v.is_truthy())
500                || combination.literal_strings.iter().any(|s| s.is_empty() || s.as_bytes() == b"0");
501
502            if has_non_truthy {
503                combination.flags.insert(CombinationFlags::GENERIC_MIXED);
504                return;
505            }
506
507            combination.flags.set_truthy_mixed(Some(true));
508        } else {
509            combination.flags.set_truthy_mixed(Some(false));
510        }
511
512        if mixed.is_falsy() {
513            if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
514                return;
515            }
516
517            combination.flags.set_mixed_from_loop_isset(Some(false));
518
519            if combination.flags.truthy_mixed().unwrap_or(false) {
520                combination.flags.insert(CombinationFlags::GENERIC_MIXED);
521                combination.flags.set_truthy_mixed(Some(false));
522                return;
523            }
524
525            if combination.flags.falsy_mixed().is_some() {
526                return;
527            }
528
529            let has_non_falsy = combination.value_types.values().any(|v| !v.is_falsy())
530                || combination.literal_strings.iter().any(|s| !s.is_empty() && s.as_bytes() != b"0");
531
532            if has_non_falsy {
533                combination.flags.insert(CombinationFlags::GENERIC_MIXED);
534                return;
535            }
536
537            combination.flags.set_falsy_mixed(Some(true));
538        } else {
539            combination.flags.set_falsy_mixed(Some(false));
540        }
541
542        if mixed.is_non_null() {
543            if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
544                return;
545            }
546
547            combination.flags.set_mixed_from_loop_isset(Some(false));
548
549            if combination.value_types.contains_key(&*ATOM_NULL) {
550                combination.flags.insert(CombinationFlags::GENERIC_MIXED);
551                return;
552            }
553
554            if combination.flags.falsy_mixed().unwrap_or(false) {
555                combination.flags.set_falsy_mixed(Some(false));
556                combination.flags.insert(CombinationFlags::GENERIC_MIXED);
557                return;
558            }
559
560            if combination.flags.nonnull_mixed().is_some() {
561                return;
562            }
563
564            combination.flags.set_mixed_from_loop_isset(Some(false));
565            combination.flags.set_nonnull_mixed(Some(true));
566        } else {
567            combination.flags.set_nonnull_mixed(Some(false));
568        }
569
570        return;
571    }
572
573    if combination.flags.falsy_mixed().unwrap_or(false) {
574        if !atomic.is_falsy() {
575            combination.flags.set_falsy_mixed(Some(false));
576            combination.flags.insert(CombinationFlags::GENERIC_MIXED);
577        }
578
579        return;
580    }
581
582    if combination.flags.truthy_mixed().unwrap_or(false) {
583        if !atomic.is_truthy() {
584            combination.flags.set_truthy_mixed(Some(false));
585            combination.flags.insert(CombinationFlags::GENERIC_MIXED);
586        }
587
588        return;
589    }
590
591    if combination.flags.nonnull_mixed().unwrap_or(false) {
592        if atomic == TAtomic::Null {
593            combination.flags.set_nonnull_mixed(Some(false));
594            combination.flags.insert(CombinationFlags::GENERIC_MIXED);
595        }
596
597        return;
598    }
599
600    if combination.flags.contains(CombinationFlags::HAS_MIXED) {
601        return;
602    }
603
604    if matches!(&atomic, TAtomic::Scalar(TScalar::Bool(bool)) if !bool.is_general())
605        && combination.value_types.contains_key(&*ATOM_BOOL)
606    {
607        return;
608    }
609
610    if let TAtomic::Resource(TResource { closed }) = atomic {
611        match closed {
612            Some(closed) => {
613                if closed {
614                    combination.flags.insert(CombinationFlags::CLOSED_RESOURCE);
615                } else {
616                    combination.flags.insert(CombinationFlags::OPEN_RESOURCE);
617                }
618            }
619            None => {
620                combination.flags.insert(CombinationFlags::RESOURCE);
621            }
622        }
623
624        return;
625    }
626
627    if matches!(&atomic, TAtomic::Scalar(TScalar::Bool(bool)) if bool.is_general()) {
628        combination.value_types.remove(&*ATOM_FALSE);
629        combination.value_types.remove(&*ATOM_TRUE);
630    }
631
632    if let TAtomic::Array(array) = atomic {
633        if options.overwrite_empty_array && array.is_empty() {
634            combination.flags.insert(CombinationFlags::HAS_EMPTY_ARRAY);
635
636            return;
637        }
638
639        // Accumulate sealed arrays for later comparison, but only up to a threshold.
640        // Once we exceed the threshold, we let the arrays fall through to be processed
641        // immediately, which generalizes them and prevents O(n²) complexity.
642        if !array.is_empty()
643            && array.is_sealed()
644            && combination.list_array_parameter.is_some()
645            && !combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY)
646            && combination.sealed_arrays.len() < options.array_combination_threshold as usize
647        {
648            combination.sealed_arrays.push(array);
649            return;
650        }
651
652        let mut sealed_arrays = vec![];
653        std::mem::swap(&mut sealed_arrays, &mut combination.sealed_arrays);
654        for array in std::iter::once(array).chain(sealed_arrays) {
655            match array {
656                TArray::List(TList { element_type, known_elements, non_empty, known_count: _ }) => {
657                    if combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY)
658                        && !combination.flags.contains(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED)
659                        && combination.keyed_array_parameters.is_none()
660                        && combination.keyed_array_entries.is_empty()
661                    {
662                        combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
663                        combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
664                        combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
665                    }
666
667                    if !non_empty {
668                        combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
669                    }
670
671                    if let Some(known_elements) = known_elements {
672                        let mut has_defined_keys = false;
673
674                        for (candidate_element_index, (candidate_optional, candidate_element_type)) in known_elements {
675                            let existing_entry = combination.list_array_entries.get(&candidate_element_index);
676
677                            let new_entry = if let Some((existing_optional, existing_type)) = existing_entry {
678                                (
679                                    *existing_optional || candidate_optional,
680                                    combine_union_types(existing_type, &candidate_element_type, codebase, options),
681                                )
682                            } else {
683                                (
684                                    candidate_optional,
685                                    if let Some(ref mut existing_value_parameter) = combination.list_array_parameter {
686                                        if !existing_value_parameter.is_never() {
687                                            *existing_value_parameter = combine_union_types(
688                                                existing_value_parameter,
689                                                &candidate_element_type,
690                                                codebase,
691                                                options,
692                                            );
693
694                                            if !candidate_optional {
695                                                has_defined_keys = true;
696                                            }
697
698                                            continue;
699                                        }
700
701                                        candidate_element_type
702                                    } else {
703                                        candidate_element_type
704                                    },
705                                )
706                            };
707
708                            combination.list_array_entries.insert(candidate_element_index, new_entry);
709
710                            if !candidate_optional {
711                                has_defined_keys = true;
712                            }
713                        }
714
715                        if !has_defined_keys {
716                            combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
717                        }
718                    } else if !options.overwrite_empty_array {
719                        if element_type.is_never() {
720                            for (pu, _) in combination.list_array_entries.values_mut() {
721                                *pu = true;
722                            }
723                        } else {
724                            for (_, entry_type) in combination.list_array_entries.values() {
725                                if let Some(ref mut existing_value_param) = combination.list_array_parameter {
726                                    *existing_value_param =
727                                        combine_union_types(existing_value_param, entry_type, codebase, options);
728                                }
729                            }
730
731                            combination.list_array_entries.clear();
732                        }
733                    }
734
735                    combination.list_array_parameter =
736                        if let Some(existing_type) = combination.list_array_parameter.as_ref() {
737                            Some(combine_union_types(existing_type, &element_type, codebase, options))
738                        } else {
739                            Some((*element_type).clone())
740                        };
741                }
742                TArray::Keyed(TKeyedArray { parameters, known_items, non_empty, known_non_list }) => {
743                    let mut had_previous_keyed_array = combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY);
744                    let sealed_budget_available = !combination.sealed_keyed_budget_exhausted
745                        && combination.sealed_arrays.len() < options.array_combination_threshold as usize;
746
747                    if !sealed_budget_available
748                        && !combination.sealed_keyed_budget_exhausted
749                        && !combination.sealed_arrays.is_empty()
750                    {
751                        flush_sealed_keyed_arrays_into_combination(combination, codebase, options);
752                        combination.sealed_keyed_budget_exhausted = true;
753                        had_previous_keyed_array = combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY);
754                    }
755
756                    if had_previous_keyed_array && sealed_budget_available {
757                        let incoming_is_sealed = parameters.is_none();
758                        let existing_is_sealed = combination.keyed_array_parameters.is_none();
759
760                        if incoming_is_sealed && !existing_is_sealed && known_items.is_some() {
761                            let known_items = widen_known_items_with_params(
762                                known_items,
763                                combination.keyed_array_parameters.as_ref(),
764                                &combination.keyed_array_entries,
765                                codebase,
766                                options,
767                            );
768
769                            combination.sealed_arrays.push(TArray::Keyed(TKeyedArray {
770                                known_items,
771                                parameters,
772                                non_empty,
773                                known_non_list,
774                            }));
775
776                            continue;
777                        }
778
779                        if !incoming_is_sealed && existing_is_sealed && !combination.keyed_array_entries.is_empty() {
780                            let mut frozen_entries = std::mem::take(&mut combination.keyed_array_entries);
781                            if let Some((key_param, value_param)) = parameters.as_ref() {
782                                for (key, (_, entry_type)) in frozen_entries.iter_mut() {
783                                    // If the incoming unsealed array also declares this key as a
784                                    // known item, the caller is saying this key is exactly the
785                                    // declared type - the generic value_param catch-all covers
786                                    // *other* keys only. Widening here would turn e.g.
787                                    // `array{count: int, id: int}` + `array{count: int, ...<string, mixed>}`
788                                    // into `array{count: mixed, id: mixed}`, which is a false loss.
789                                    if known_items.as_ref().is_some_and(|ki| ki.contains_key(key)) {
790                                        continue;
791                                    }
792
793                                    let key_type = TUnion::from_atomic(key.to_atomic());
794
795                                    if union_comparator::can_expression_types_be_identical(
796                                        codebase, &key_type, key_param, false, false,
797                                    ) {
798                                        *entry_type = combine_union_types(entry_type, value_param, codebase, options);
799                                    }
800                                }
801                            }
802
803                            let frozen = TArray::Keyed(TKeyedArray {
804                                known_items: Some(frozen_entries),
805                                parameters: None,
806                                non_empty: combination.flags.contains(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED),
807                                known_non_list: combination
808                                    .flags
809                                    .contains(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST),
810                            });
811                            combination.sealed_arrays.push(frozen);
812                            combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
813                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST);
814                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
815                            combination.flags.insert(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
816                            had_previous_keyed_array = false;
817                        }
818
819                        if incoming_is_sealed
820                            && existing_is_sealed
821                            && !combination.keyed_array_entries.is_empty()
822                            && let Some(known_items_inner) = known_items.as_ref()
823                            && combination.sealed_arrays.len() + 1 < options.array_combination_threshold as usize
824                            && (!known_items_inner.keys().any(|k| combination.keyed_array_entries.contains_key(k))
825                                || shapes_are_discriminated(
826                                    known_items_inner,
827                                    &combination.keyed_array_entries,
828                                    codebase,
829                                ))
830                        {
831                            let frozen = TArray::Keyed(TKeyedArray {
832                                known_items: Some(std::mem::take(&mut combination.keyed_array_entries)),
833                                parameters: None,
834                                non_empty: combination.flags.contains(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED),
835                                known_non_list: combination
836                                    .flags
837                                    .contains(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST),
838                            });
839                            combination.sealed_arrays.push(frozen);
840                            combination.sealed_arrays.push(TArray::Keyed(TKeyedArray {
841                                known_items,
842                                parameters,
843                                non_empty,
844                                known_non_list,
845                            }));
846                            combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
847                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST);
848                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
849                            combination.flags.insert(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
850
851                            continue;
852                        }
853                    }
854
855                    combination.flags.insert(CombinationFlags::HAS_KEYED_ARRAY);
856                    if known_non_list {
857                        combination.flags.insert(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST);
858                    }
859
860                    if non_empty {
861                        combination.flags.insert(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
862                    } else {
863                        combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
864
865                        if parameters.is_none()
866                            && known_items.as_ref().is_none_or(|items| items.is_empty())
867                            && combination.list_array_parameter.is_some()
868                        {
869                            combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
870                            for (is_optional, _) in combination.list_array_entries.values_mut() {
871                                *is_optional = true;
872                            }
873
874                            had_previous_keyed_array = false;
875                            combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
876                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST);
877
878                            continue;
879                        }
880                    }
881
882                    if let Some(known_items) = known_items {
883                        let has_existing_entries =
884                            !combination.keyed_array_entries.is_empty() || had_previous_keyed_array;
885                        let mut possibly_undefined_entries =
886                            combination.keyed_array_entries.keys().copied().collect::<HashSet<_>>();
887
888                        let mut has_defined_keys = false;
889
890                        for (candidate_item_name, (cu, candidate_item_type)) in known_items {
891                            if let Some((eu, existing_type)) =
892                                combination.keyed_array_entries.get_mut(&candidate_item_name)
893                            {
894                                if cu {
895                                    *eu = true;
896                                }
897                                if &candidate_item_type != existing_type {
898                                    *existing_type =
899                                        combine_union_types(existing_type, &candidate_item_type, codebase, options);
900                                }
901                            } else {
902                                let new_item_value_type =
903                                    if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
904                                        combination.keyed_array_parameters
905                                    {
906                                        adjust_keyed_array_parameters(
907                                            existing_value_param,
908                                            &candidate_item_type,
909                                            codebase,
910                                            options,
911                                            &candidate_item_name,
912                                            existing_key_param,
913                                        );
914
915                                        continue;
916                                    } else {
917                                        let new_type = candidate_item_type.clone();
918                                        (has_existing_entries || cu, new_type)
919                                    };
920
921                                combination.keyed_array_entries.insert(candidate_item_name, new_item_value_type);
922                            }
923
924                            possibly_undefined_entries.remove(&candidate_item_name);
925
926                            if !cu {
927                                has_defined_keys = true;
928                            }
929                        }
930
931                        if !has_defined_keys {
932                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
933                        }
934
935                        for possibly_undefined_type_key in possibly_undefined_entries {
936                            let possibly_undefined_type =
937                                combination.keyed_array_entries.get_mut(&possibly_undefined_type_key);
938                            if let Some((pu, _)) = possibly_undefined_type {
939                                *pu = true;
940                            }
941                        }
942                    } else if !options.overwrite_empty_array {
943                        if match &parameters {
944                            Some((_, value_param)) => value_param.is_never(),
945                            None => true,
946                        } {
947                            for (tu, _) in combination.keyed_array_entries.values_mut() {
948                                *tu = true;
949                            }
950                        } else {
951                            for (key, (_, entry_type)) in &combination.keyed_array_entries {
952                                if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
953                                    combination.keyed_array_parameters
954                                {
955                                    adjust_keyed_array_parameters(
956                                        existing_value_param,
957                                        entry_type,
958                                        codebase,
959                                        options,
960                                        key,
961                                        existing_key_param,
962                                    );
963                                }
964                            }
965
966                            combination.keyed_array_entries.clear();
967                        }
968                    }
969
970                    combination.keyed_array_parameters = match (&combination.keyed_array_parameters, parameters) {
971                        (None, None) => None,
972                        (Some(existing_types), None) => Some(existing_types.clone()),
973                        (None, Some(params)) => Some(((*params.0).clone(), (*params.1).clone())),
974                        (Some(existing_types), Some(params)) => Some((
975                            combine_union_types(&existing_types.0, &params.0, codebase, options),
976                            combine_union_types(&existing_types.1, &params.1, codebase, options),
977                        )),
978                    };
979                }
980            }
981        }
982
983        return;
984    }
985
986    // this probably won't ever happen, but the object top type
987    // can eliminate variants
988    if atomic == TAtomic::Object(TObject::Any) {
989        combination.flags.insert(CombinationFlags::HAS_OBJECT_TOP_TYPE);
990        combination.value_types.retain(|_, t| !matches!(t, TAtomic::Object(TObject::Named(_))));
991        combination.value_types.insert(atomic.get_id(), atomic);
992
993        return;
994    }
995
996    if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
997        if let Some(object_static) = combination.object_static.get(&named_object.get_name()) {
998            if *object_static && !named_object.is_static {
999                combination.object_static.insert(named_object.get_name(), false);
1000            }
1001        } else {
1002            combination.object_static.insert(named_object.get_name(), named_object.is_static);
1003        }
1004    }
1005
1006    if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
1007        let fq_class_name = named_object.get_name();
1008        if let Some(type_parameters) = named_object.get_type_parameters() {
1009            let object_type_key = get_combiner_key(fq_class_name, type_parameters, codebase);
1010
1011            if let Some((_, existing_type_params)) = combination.object_type_params.get(&object_type_key) {
1012                let mut new_type_parameters = Vec::with_capacity(type_parameters.len());
1013                for (i, type_param) in type_parameters.iter().enumerate() {
1014                    if let Some(existing_type_param) = existing_type_params.get(i) {
1015                        new_type_parameters.push(combine_union_types(
1016                            existing_type_param,
1017                            type_param,
1018                            codebase,
1019                            options,
1020                        ));
1021                    }
1022                }
1023
1024                combination.object_type_params.insert(object_type_key, (fq_class_name, new_type_parameters));
1025            } else {
1026                combination.object_type_params.insert(object_type_key, (fq_class_name, type_parameters.to_vec()));
1027            }
1028
1029            return;
1030        }
1031    }
1032
1033    if let TAtomic::Object(TObject::Enum(enum_object)) = atomic {
1034        combination.enum_names.insert((enum_object.get_name(), enum_object.get_case()));
1035
1036        return;
1037    }
1038
1039    if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
1040        let fq_class_name = named_object.get_name();
1041        let intersection_types = named_object.get_intersection_types();
1042
1043        if combination.flags.contains(CombinationFlags::HAS_OBJECT_TOP_TYPE)
1044            || combination.value_types.contains_key(&atomic.get_id())
1045        {
1046            return;
1047        }
1048
1049        let Some(symbol_type) = codebase.symbols.get_kind(fq_class_name) else {
1050            combination.value_types.insert(atomic.get_id(), atomic);
1051            return;
1052        };
1053
1054        if !matches!(symbol_type, SymbolKind::Class | SymbolKind::Enum | SymbolKind::Interface) {
1055            combination.value_types.insert(atomic.get_id(), atomic);
1056            return;
1057        }
1058
1059        let is_class = matches!(symbol_type, SymbolKind::Class);
1060        let is_interface = matches!(symbol_type, SymbolKind::Interface);
1061
1062        let mut types_to_remove: Vec<Word> = Vec::new();
1063
1064        for (key, existing_type) in &combination.value_types {
1065            if let TAtomic::Object(TObject::Named(existing_object)) = &existing_type {
1066                let existing_name = existing_object.get_name();
1067
1068                if intersection_types.is_some() || existing_object.has_intersection_types() {
1069                    if object_comparator::is_shallowly_contained_by(
1070                        codebase,
1071                        existing_type,
1072                        &atomic,
1073                        false,
1074                        &mut ComparisonResult::new(),
1075                    ) {
1076                        types_to_remove.push(existing_name);
1077                        continue;
1078                    }
1079
1080                    if object_comparator::is_shallowly_contained_by(
1081                        codebase,
1082                        &atomic,
1083                        existing_type,
1084                        false,
1085                        &mut ComparisonResult::new(),
1086                    ) {
1087                        return;
1088                    }
1089
1090                    continue;
1091                }
1092
1093                let Some(existing_symbol_kind) = codebase.symbols.get_kind(existing_object.get_name()) else {
1094                    continue;
1095                };
1096
1097                if matches!(existing_symbol_kind, SymbolKind::Class) {
1098                    // remove subclasses
1099                    if codebase.is_instance_of(existing_name.as_bytes(), fq_class_name.as_bytes()) {
1100                        types_to_remove.push(*key);
1101                        continue;
1102                    }
1103
1104                    if is_class {
1105                        // if covered by a parent class
1106                        if codebase.class_extends(fq_class_name.as_bytes(), existing_name.as_bytes()) {
1107                            return;
1108                        }
1109                    } else if is_interface {
1110                        // if covered by a parent class
1111                        if codebase.class_implements(fq_class_name.as_bytes(), existing_name.as_bytes()) {
1112                            return;
1113                        }
1114                    }
1115                } else if matches!(existing_symbol_kind, SymbolKind::Interface) {
1116                    if codebase.class_implements(existing_name.as_bytes(), fq_class_name.as_bytes()) {
1117                        types_to_remove.push(existing_name);
1118                        continue;
1119                    }
1120
1121                    if (is_class || is_interface)
1122                        && codebase.class_implements(fq_class_name.as_bytes(), existing_name.as_bytes())
1123                    {
1124                        return;
1125                    }
1126                }
1127            }
1128        }
1129
1130        combination.value_types.insert(atomic.get_id(), atomic);
1131
1132        for type_key in types_to_remove {
1133            combination.value_types.remove(&type_key);
1134        }
1135
1136        return;
1137    }
1138
1139    if atomic == TAtomic::Scalar(TScalar::Generic) {
1140        combination.literal_strings.clear();
1141        combination.integers.clear();
1142        combination.literal_floats.clear();
1143        combination.value_types.retain(|k, _| {
1144            k.as_bytes() != b"string"
1145                && k.as_bytes() != b"bool"
1146                && k.as_bytes() != b"false"
1147                && k.as_bytes() != b"true"
1148                && k.as_bytes() != b"float"
1149                && k.as_bytes() != b"numeric"
1150                && k.as_bytes() != b"array-key"
1151        });
1152
1153        combination.value_types.insert(atomic.get_id(), atomic);
1154        return;
1155    }
1156
1157    if atomic == TAtomic::Scalar(TScalar::ArrayKey) {
1158        if combination.value_types.contains_key(&*ATOM_SCALAR) {
1159            return;
1160        }
1161
1162        combination.literal_strings.clear();
1163        combination.integers.clear();
1164        combination.value_types.retain(|k, _| k != &*ATOM_STRING && k != &*ATOM_INT);
1165        combination.value_types.insert(atomic.get_id(), atomic);
1166
1167        return;
1168    }
1169
1170    if let TAtomic::Scalar(TScalar::String(_) | TScalar::Integer(_)) = atomic
1171        && (combination.value_types.contains_key(&*ATOM_SCALAR)
1172            || combination.value_types.contains_key(&*ATOM_ARRAY_KEY))
1173    {
1174        return;
1175    }
1176
1177    if let TAtomic::Scalar(TScalar::Float(_) | TScalar::Integer(_)) = atomic
1178        && (combination.value_types.contains_key(&*ATOM_NUMERIC) || combination.value_types.contains_key(&*ATOM_SCALAR))
1179    {
1180        return;
1181    }
1182
1183    if let TAtomic::Scalar(TScalar::String(mut string_scalar)) = atomic {
1184        if let Some(existing_string_type) = combination.value_types.get_mut(&*ATOM_STRING) {
1185            if let TAtomic::Scalar(TScalar::String(existing_string_type)) = existing_string_type {
1186                if let Some(lit_atom) = string_scalar.get_known_literal_atom() {
1187                    let lit_value = lit_atom.as_bytes();
1188                    let is_incompatible = (existing_string_type.is_numeric && !str_is_numeric(lit_value))
1189                        || (existing_string_type.is_truthy && (lit_value.is_empty() || lit_value == b"0"))
1190                        || (existing_string_type.is_non_empty && lit_value.is_empty())
1191                        || (existing_string_type.is_lowercase() && lit_value.iter().any(u8::is_ascii_uppercase))
1192                        || (existing_string_type.is_uppercase() && lit_value.iter().any(u8::is_ascii_lowercase));
1193
1194                    if is_incompatible {
1195                        // Check threshold before adding literal string
1196                        if combination.literal_strings.len() >= options.string_combination_threshold as usize {
1197                            // Exceeded threshold - just merge into the base string type
1198                            *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1199                        } else {
1200                            combination.literal_strings.insert(lit_atom);
1201                        }
1202                    } else {
1203                        *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1204                    }
1205                } else {
1206                    *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1207                }
1208            }
1209        } else if let Some(atom) = string_scalar.get_known_literal_atom() {
1210            // Check threshold before adding literal string
1211            if combination.literal_strings.len() >= options.string_combination_threshold as usize {
1212                // Exceeded threshold - generalize to base string type
1213                combination.literal_strings.clear();
1214                combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::string()));
1215            } else {
1216                combination.literal_strings.insert(atom);
1217            }
1218        } else {
1219            let mut literals_to_keep = WordSet::default();
1220            if !combination.literal_strings.is_empty() {
1221                string_scalar.is_callable = false;
1222            }
1223
1224            if string_scalar.is_truthy
1225                || string_scalar.is_non_empty
1226                || string_scalar.is_numeric
1227                || !string_scalar.casing.is_unspecified()
1228            {
1229                for value in &combination.literal_strings {
1230                    if value.is_empty() {
1231                        string_scalar.is_non_empty = false;
1232                        string_scalar.is_truthy = false;
1233                        string_scalar.is_numeric = false;
1234                        break;
1235                    } else if value.as_bytes() == b"0" {
1236                        string_scalar.is_truthy = false;
1237                    }
1238
1239                    if string_scalar.is_numeric && !str_is_numeric(value.as_bytes()) {
1240                        literals_to_keep.insert(*value);
1241                    } else {
1242                        string_scalar.is_numeric = string_scalar.is_numeric && str_is_numeric(value.as_bytes());
1243                    }
1244
1245                    string_scalar.casing = match string_scalar.casing {
1246                        TStringCasing::Lowercase if value.as_bytes().iter().all(u8::is_ascii_lowercase) => {
1247                            TStringCasing::Lowercase
1248                        }
1249                        TStringCasing::Uppercase if value.as_bytes().iter().all(u8::is_ascii_uppercase) => {
1250                            TStringCasing::Uppercase
1251                        }
1252                        _ => TStringCasing::Unspecified,
1253                    };
1254                }
1255            }
1256
1257            combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::String(string_scalar)));
1258
1259            std::mem::swap(&mut combination.literal_strings, &mut literals_to_keep);
1260        }
1261
1262        return;
1263    }
1264
1265    if let TAtomic::Scalar(TScalar::Integer(integer)) = &atomic {
1266        // If we already have the base int type, no need to track literals
1267        if combination.value_types.contains_key(&*ATOM_INT) {
1268            return;
1269        }
1270
1271        // Check if adding this integer would exceed the threshold
1272        if integer.is_literal() && combination.integers.len() >= options.integer_combination_threshold as usize {
1273            // Exceeded threshold - generalize to base int type
1274            combination.integers.clear();
1275            combination.value_types.insert(*ATOM_INT, TAtomic::Scalar(TScalar::int()));
1276            return;
1277        }
1278
1279        combination.integers.push(*integer);
1280
1281        return;
1282    }
1283
1284    if let TAtomic::Scalar(TScalar::Float(float_scalar)) = &atomic {
1285        if let Some(stored) = combination.value_types.get(&*ATOM_FLOAT) {
1286            if matches!(stored, TAtomic::Scalar(TScalar::Float(TFloat::Float))) {
1287                return;
1288            }
1289
1290            if matches!(float_scalar, TFloat::Float) {
1291                combination.literal_floats.clear();
1292                combination.value_types.insert(*ATOM_FLOAT, atomic);
1293            }
1294
1295            return;
1296        }
1297
1298        if let TFloat::Literal(literal_value) = float_scalar {
1299            if combination.literal_floats.len() >= options.string_combination_threshold as usize {
1300                combination.literal_floats.clear();
1301                combination.value_types.insert(*ATOM_FLOAT, TAtomic::Scalar(TScalar::float()));
1302                return;
1303            }
1304            combination.literal_floats.push(*literal_value);
1305        } else {
1306            combination.literal_floats.clear();
1307            combination.value_types.insert(*ATOM_FLOAT, atomic);
1308        }
1309
1310        return;
1311    }
1312
1313    combination.value_types.insert(atomic.get_id(), atomic);
1314}
1315
1316fn shapes_are_discriminated(
1317    incoming: &BTreeMap<ArrayKey, (bool, TUnion)>,
1318    existing: &BTreeMap<ArrayKey, (bool, TUnion)>,
1319    codebase: &CodebaseMetadata,
1320) -> bool {
1321    let mut has_asymmetric_keys = false;
1322    for key in incoming.keys() {
1323        if !existing.contains_key(key) {
1324            has_asymmetric_keys = true;
1325            break;
1326        }
1327    }
1328
1329    if !has_asymmetric_keys {
1330        for key in existing.keys() {
1331            if !incoming.contains_key(key) {
1332                has_asymmetric_keys = true;
1333                break;
1334            }
1335        }
1336    }
1337
1338    if !has_asymmetric_keys {
1339        return false;
1340    }
1341
1342    for (key, (incoming_optional, incoming_type)) in incoming {
1343        if *incoming_optional {
1344            continue;
1345        }
1346
1347        let Some((existing_optional, existing_type)) = existing.get(key) else {
1348            continue;
1349        };
1350
1351        if *existing_optional {
1352            continue;
1353        }
1354
1355        if !union_comparator::can_expression_types_be_identical(codebase, incoming_type, existing_type, false, false) {
1356            return true;
1357        }
1358    }
1359
1360    false
1361}
1362
1363/// Widens known items in a sealed array with the generic value type from parameters.
1364/// This is needed when combining a sealed array with a parametric one, the parametric
1365/// array's generic string keys could overwrite any of the sealed array's known keys.
1366fn widen_known_items_with_params(
1367    known_items: Option<BTreeMap<ArrayKey, (bool, TUnion)>>,
1368    params: Option<&(TUnion, TUnion)>,
1369    other_known_items: &BTreeMap<ArrayKey, (bool, TUnion)>,
1370    codebase: &CodebaseMetadata,
1371    options: CombinerOptions,
1372) -> Option<BTreeMap<ArrayKey, (bool, TUnion)>> {
1373    let mut items = known_items?;
1374
1375    if let Some((key_param, value_param)) = params {
1376        let (key_param_accepts_int, key_param_accepts_string) =
1377            if key_param.has_mixed() || key_param.has_mixed_template() {
1378                (true, true)
1379            } else {
1380                let mut accepts_int = false;
1381                let mut accepts_string = false;
1382                for part in key_param.types.as_ref() {
1383                    if accepts_int && accepts_string {
1384                        break;
1385                    }
1386
1387                    match part {
1388                        TAtomic::Scalar(TScalar::ArrayKey) => {
1389                            accepts_int = true;
1390                            accepts_string = true;
1391                        }
1392                        TAtomic::Scalar(TScalar::Integer(_)) => accepts_int = true,
1393                        TAtomic::Scalar(TScalar::String(_)) => accepts_string = true,
1394                        _ => {
1395                            accepts_int = true;
1396                            accepts_string = true;
1397                        }
1398                    }
1399                }
1400
1401                (accepts_int, accepts_string)
1402            };
1403
1404        if !key_param_accepts_int && !key_param_accepts_string {
1405            return Some(items);
1406        }
1407
1408        for (key, (_, entry_type)) in items.iter_mut() {
1409            if entry_type == value_param {
1410                continue;
1411            }
1412
1413            if other_known_items.contains_key(key) {
1414                continue;
1415            }
1416
1417            let key_compatible = match key {
1418                ArrayKey::Integer(_) => key_param_accepts_int,
1419                ArrayKey::String(_) => key_param_accepts_string,
1420                ArrayKey::ClassLikeConstant { .. } => key_param_accepts_int || key_param_accepts_string,
1421            };
1422
1423            if !key_compatible {
1424                continue;
1425            }
1426
1427            *entry_type = combine_union_types(entry_type, value_param, codebase, options);
1428        }
1429    }
1430
1431    Some(items)
1432}
1433
1434fn adjust_keyed_array_parameters(
1435    existing_value_param: &mut TUnion,
1436    entry_type: &TUnion,
1437    codebase: &CodebaseMetadata,
1438    options: CombinerOptions,
1439    key: &ArrayKey,
1440    existing_key_param: &mut TUnion,
1441) {
1442    *existing_value_param = combine_union_types(existing_value_param, entry_type, codebase, options);
1443    let new_key_type = key.to_union();
1444    *existing_key_param = combine_union_types(existing_key_param, &new_key_type, codebase, options);
1445}
1446
1447fn flush_sealed_keyed_arrays_into_combination(
1448    combination: &mut TypeCombination,
1449    codebase: &CodebaseMetadata,
1450    options: CombinerOptions,
1451) {
1452    let sealed = std::mem::take(&mut combination.sealed_arrays);
1453    let mut any_keyed = false;
1454    let mut put_back = Vec::new();
1455
1456    for array in sealed {
1457        let TArray::Keyed(keyed) = array else {
1458            put_back.push(array);
1459            continue;
1460        };
1461
1462        any_keyed = true;
1463        let TKeyedArray { known_items, parameters, non_empty, known_non_list } = keyed;
1464
1465        if known_non_list {
1466            combination.flags.insert(CombinationFlags::KEYED_ARRAY_KNOWN_NON_LIST);
1467        }
1468
1469        if non_empty {
1470            combination.flags.insert(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
1471        } else {
1472            combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
1473        }
1474
1475        if let Some(known_items) = known_items {
1476            for (candidate_item_name, (candidate_optional, candidate_item_type)) in known_items {
1477                if let Some((existing_optional, existing_type)) =
1478                    combination.keyed_array_entries.get_mut(&candidate_item_name)
1479                {
1480                    if candidate_optional {
1481                        *existing_optional = true;
1482                    }
1483                    if &candidate_item_type != existing_type {
1484                        *existing_type = combine_union_types(existing_type, &candidate_item_type, codebase, options);
1485                    }
1486                } else {
1487                    let inserted = if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
1488                        combination.keyed_array_parameters
1489                    {
1490                        adjust_keyed_array_parameters(
1491                            existing_value_param,
1492                            &candidate_item_type,
1493                            codebase,
1494                            options,
1495                            &candidate_item_name,
1496                            existing_key_param,
1497                        );
1498                        None
1499                    } else {
1500                        Some((true, candidate_item_type.clone()))
1501                    };
1502
1503                    if let Some(entry) = inserted {
1504                        combination.keyed_array_entries.insert(candidate_item_name, entry);
1505                    }
1506                }
1507            }
1508        }
1509
1510        combination.keyed_array_parameters = match (combination.keyed_array_parameters.take(), parameters) {
1511            (None, None) => None,
1512            (Some(existing_types), None) => Some(existing_types),
1513            (None, Some(params)) => Some(((*params.0).clone(), (*params.1).clone())),
1514            (Some(existing_types), Some(params)) => Some((
1515                combine_union_types(&existing_types.0, &params.0, codebase, options),
1516                combine_union_types(&existing_types.1, &params.1, codebase, options),
1517            )),
1518        };
1519    }
1520
1521    if any_keyed {
1522        combination.flags.insert(CombinationFlags::HAS_KEYED_ARRAY);
1523    }
1524
1525    combination.sealed_arrays = put_back;
1526}
1527
1528const COMBINER_KEY_STACK_BUF: usize = 256;
1529
1530fn get_combiner_key(name: Word, type_params: &[TUnion], codebase: &CodebaseMetadata) -> Word {
1531    let covariants = if let Some(class_like_metadata) = codebase.get_class_like(name.as_bytes()) {
1532        &class_like_metadata.template_variance
1533    } else {
1534        return name;
1535    };
1536
1537    let name_str = name.as_bytes();
1538    let mut estimated_len = name_str.len() + 2; // name + "<" + ">"
1539    for (i, tunion) in type_params.iter().enumerate() {
1540        if i > 0 {
1541            estimated_len += 2; // ", "
1542        }
1543
1544        if covariants.get(i) == Some(&Variance::Covariant) {
1545            estimated_len += 1; // "*"
1546        } else {
1547            estimated_len += tunion.get_id().len();
1548        }
1549    }
1550
1551    if estimated_len <= COMBINER_KEY_STACK_BUF {
1552        let mut buffer = [0u8; COMBINER_KEY_STACK_BUF];
1553        let mut pos = 0;
1554
1555        buffer[pos..pos + name_str.len()].copy_from_slice(name_str);
1556        pos += name_str.len();
1557
1558        buffer[pos] = b'<';
1559        pos += 1;
1560
1561        for (i, tunion) in type_params.iter().enumerate() {
1562            if i > 0 {
1563                buffer[pos..pos + 2].copy_from_slice(b", ");
1564                pos += 2;
1565            }
1566            let id_word = tunion.get_id();
1567            let param_bytes: &[u8] =
1568                if covariants.get(i) == Some(&Variance::Covariant) { b"*" } else { id_word.as_bytes() };
1569            let need = param_bytes.len();
1570            buffer[pos..pos + need].copy_from_slice(param_bytes);
1571            pos += need;
1572        }
1573
1574        buffer[pos] = b'>';
1575        pos += 1;
1576
1577        return word(&buffer[..pos]);
1578    }
1579
1580    let mut result: Vec<u8> = Vec::with_capacity(estimated_len);
1581    result.extend_from_slice(name_str);
1582    result.push(b'<');
1583    for (i, tunion) in type_params.iter().enumerate() {
1584        if i > 0 {
1585            result.extend_from_slice(b", ");
1586        }
1587        if covariants.get(i) == Some(&Variance::Covariant) {
1588            result.push(b'*');
1589        } else {
1590            result.extend_from_slice(tunion.get_id().as_bytes());
1591        }
1592    }
1593    result.push(b'>');
1594    word(&result)
1595}
1596
1597fn combine_string_scalars(s1: &TString, s2: TString) -> TString {
1598    TString {
1599        literal: match (&s1.literal, s2.literal) {
1600            (Some(TStringLiteral::Value(v1)), Some(TStringLiteral::Value(v2))) => {
1601                if v1 == &v2 {
1602                    Some(TStringLiteral::Value(v2))
1603                } else {
1604                    Some(TStringLiteral::Unspecified)
1605                }
1606            }
1607            (Some(TStringLiteral::Unspecified), Some(_)) | (Some(_), Some(TStringLiteral::Unspecified)) => {
1608                Some(TStringLiteral::Unspecified)
1609            }
1610            _ => None,
1611        },
1612        is_numeric: s1.is_numeric && s2.is_numeric,
1613        is_truthy: s1.is_truthy && s2.is_truthy,
1614        is_non_empty: s1.is_non_empty && s2.is_non_empty,
1615        is_callable: s1.is_callable && s2.is_callable,
1616        casing: match (s1.casing, s2.casing) {
1617            (TStringCasing::Lowercase, TStringCasing::Lowercase) => TStringCasing::Lowercase,
1618            (TStringCasing::Uppercase, TStringCasing::Uppercase) => TStringCasing::Uppercase,
1619            _ => TStringCasing::Unspecified,
1620        },
1621    }
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626    use std::collections::BTreeMap;
1627
1628    use super::*;
1629
1630    use crate::ttype::atomic::TAtomic;
1631    use crate::ttype::atomic::array::list::TList;
1632    use crate::ttype::atomic::scalar::TScalar;
1633
1634    #[test]
1635    fn test_combine_scalars() {
1636        let types = vec![
1637            TAtomic::Scalar(TScalar::string()),
1638            TAtomic::Scalar(TScalar::int()),
1639            TAtomic::Scalar(TScalar::float()),
1640            TAtomic::Scalar(TScalar::bool()),
1641        ];
1642
1643        let combined =
1644            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1645
1646        assert_eq!(combined.len(), 1);
1647        assert!(matches!(combined[0], TAtomic::Scalar(TScalar::Generic)));
1648    }
1649
1650    #[test]
1651    fn test_combine_boolean_lists() {
1652        let types = vec![
1653            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1654                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#false())))),
1655                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#true())))),
1656            ])))),
1657            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1658                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#true())))),
1659                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#false())))),
1660            ])))),
1661        ];
1662
1663        let combined =
1664            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1665
1666        assert_eq!(combined.len(), 2);
1667        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1668        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1669    }
1670
1671    #[test]
1672    fn test_combine_integer_lists() {
1673        let types = vec![
1674            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1675                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1676                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1677            ])))),
1678            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1679                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1680                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1681            ])))),
1682        ];
1683
1684        let combined =
1685            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1686
1687        assert_eq!(combined.len(), 2);
1688        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1689        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1690    }
1691
1692    #[test]
1693    fn test_combine_string_lists() {
1694        let types = vec![
1695            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1696                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1697                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1698            ])))),
1699            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1700                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1701                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1702            ])))),
1703        ];
1704
1705        let combined =
1706            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1707
1708        assert_eq!(combined.len(), 2);
1709        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1710        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1711    }
1712
1713    #[test]
1714    fn test_combine_mixed_literal_lists() {
1715        let types = vec![
1716            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1717                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1718                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1719            ])))),
1720            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1721                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1722                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1723            ])))),
1724        ];
1725
1726        let combined =
1727            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1728
1729        assert_eq!(combined.len(), 2);
1730        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1731        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1732    }
1733
1734    #[test]
1735    fn test_combine_list_with_generic_list() {
1736        let types = vec![
1737            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1738                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1739                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1740            ])))),
1741            TAtomic::Array(TArray::List(TList::new(Arc::new(TUnion::from_atomic(TAtomic::Scalar(TScalar::int())))))), // list<int>
1742        ];
1743
1744        let combined =
1745            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1746
1747        // Expecting list{1,2} and list<int> = list<int>
1748        assert_eq!(combined.len(), 1);
1749
1750        let TAtomic::Array(TArray::List(list_type)) = &combined[0] else {
1751            panic!("Expected a list type");
1752        };
1753
1754        let Some(known_elements) = &list_type.known_elements else {
1755            panic!("Expected known elements");
1756        };
1757
1758        assert!(!list_type.is_non_empty());
1759        assert!(list_type.known_count.is_none());
1760        assert!(list_type.element_type.is_int());
1761
1762        assert_eq!(known_elements.len(), 2);
1763        assert!(known_elements.contains_key(&0));
1764        assert!(known_elements.contains_key(&1));
1765
1766        let Some(first_element) = known_elements.get(&0) else {
1767            panic!("Expected first element");
1768        };
1769
1770        let Some(second_element) = known_elements.get(&1) else {
1771            panic!("Expected second element");
1772        };
1773
1774        assert!(first_element.1.is_int());
1775        assert!(second_element.1.is_int());
1776    }
1777}