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