Skip to main content

mago_codex/ttype/
combiner.rs

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