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