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