Skip to main content

mago_codex/ttype/
combiner.rs

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