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