Skip to main content

mago_codex/ttype/
combiner.rs

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