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 combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY)
622                        && !combination.flags.contains(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED)
623                        && combination.keyed_array_parameters.is_none()
624                        && combination.keyed_array_entries.is_empty()
625                    {
626                        combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
627                        combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
628                        combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
629                    }
630
631                    if !non_empty {
632                        combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
633                    }
634
635                    if let Some(known_elements) = known_elements {
636                        let mut has_defined_keys = false;
637
638                        for (candidate_element_index, (candidate_optional, candidate_element_type)) in known_elements {
639                            let existing_entry = combination.list_array_entries.get(&candidate_element_index);
640
641                            let new_entry = if let Some((existing_optional, existing_type)) = existing_entry {
642                                (
643                                    *existing_optional || candidate_optional,
644                                    combine_union_types(existing_type, &candidate_element_type, codebase, options),
645                                )
646                            } else {
647                                (
648                                    candidate_optional,
649                                    if let Some(ref mut existing_value_parameter) = combination.list_array_parameter {
650                                        if !existing_value_parameter.is_never() {
651                                            *existing_value_parameter = combine_union_types(
652                                                existing_value_parameter,
653                                                &candidate_element_type,
654                                                codebase,
655                                                options,
656                                            );
657
658                                            if !candidate_optional {
659                                                has_defined_keys = true;
660                                            }
661
662                                            continue;
663                                        }
664
665                                        candidate_element_type
666                                    } else {
667                                        candidate_element_type
668                                    },
669                                )
670                            };
671
672                            combination.list_array_entries.insert(candidate_element_index, new_entry);
673
674                            if !candidate_optional {
675                                has_defined_keys = true;
676                            }
677                        }
678
679                        if !has_defined_keys {
680                            combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
681                        }
682                    } else if !options.overwrite_empty_array {
683                        if element_type.is_never() {
684                            for (pu, _) in combination.list_array_entries.values_mut() {
685                                *pu = true;
686                            }
687                        } else {
688                            for (_, entry_type) in combination.list_array_entries.values() {
689                                if let Some(ref mut existing_value_param) = combination.list_array_parameter {
690                                    *existing_value_param =
691                                        combine_union_types(existing_value_param, entry_type, codebase, options);
692                                }
693                            }
694
695                            combination.list_array_entries.clear();
696                        }
697                    }
698
699                    combination.list_array_parameter =
700                        if let Some(existing_type) = combination.list_array_parameter.as_ref() {
701                            Some(combine_union_types(existing_type, &element_type, codebase, options))
702                        } else {
703                            Some((*element_type).clone())
704                        };
705                }
706                TArray::Keyed(TKeyedArray { parameters, known_items, non_empty }) => {
707                    let mut had_previous_keyed_array = combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY);
708                    let sealed_budget_available = !combination.sealed_keyed_budget_exhausted
709                        && combination.sealed_arrays.len() < options.array_combination_threshold as usize;
710
711                    if !sealed_budget_available
712                        && !combination.sealed_keyed_budget_exhausted
713                        && !combination.sealed_arrays.is_empty()
714                    {
715                        flush_sealed_keyed_arrays_into_combination(combination, codebase, options);
716                        combination.sealed_keyed_budget_exhausted = true;
717                        had_previous_keyed_array = combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY);
718                    }
719
720                    if had_previous_keyed_array && sealed_budget_available {
721                        let incoming_is_sealed = parameters.is_none();
722                        let existing_is_sealed = combination.keyed_array_parameters.is_none();
723
724                        if incoming_is_sealed && !existing_is_sealed && known_items.is_some() {
725                            let known_items = widen_known_items_with_params(
726                                known_items,
727                                combination.keyed_array_parameters.as_ref(),
728                                &combination.keyed_array_entries,
729                                codebase,
730                                options,
731                            );
732
733                            combination.sealed_arrays.push(TArray::Keyed(TKeyedArray {
734                                known_items,
735                                parameters,
736                                non_empty,
737                            }));
738
739                            continue;
740                        }
741
742                        if !incoming_is_sealed && existing_is_sealed && !combination.keyed_array_entries.is_empty() {
743                            let mut frozen_entries = std::mem::take(&mut combination.keyed_array_entries);
744                            if let Some((key_param, value_param)) = parameters.as_ref() {
745                                for (key, (_, entry_type)) in frozen_entries.iter_mut() {
746                                    // If the incoming unsealed array also declares this key as a
747                                    // known item, the caller is saying this key is exactly the
748                                    // declared type - the generic value_param catch-all covers
749                                    // *other* keys only. Widening here would turn e.g.
750                                    // `array{count: int, id: int}` + `array{count: int, ...<string, mixed>}`
751                                    // into `array{count: mixed, id: mixed}`, which is a false loss.
752                                    if known_items.as_ref().is_some_and(|ki| ki.contains_key(key)) {
753                                        continue;
754                                    }
755
756                                    let key_type = TUnion::from_atomic(key.to_atomic());
757
758                                    if union_comparator::can_expression_types_be_identical(
759                                        codebase, &key_type, key_param, false, false,
760                                    ) {
761                                        *entry_type = combine_union_types(entry_type, value_param, codebase, options);
762                                    }
763                                }
764                            }
765
766                            let frozen = TArray::Keyed(TKeyedArray {
767                                known_items: Some(frozen_entries),
768                                parameters: None,
769                                non_empty: combination.flags.contains(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED),
770                            });
771                            combination.sealed_arrays.push(frozen);
772                            combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
773                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
774                            combination.flags.insert(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
775                            had_previous_keyed_array = false;
776                        }
777
778                        if incoming_is_sealed
779                            && existing_is_sealed
780                            && !combination.keyed_array_entries.is_empty()
781                            && let Some(known_items_inner) = known_items.as_ref()
782                            && combination.sealed_arrays.len() + 1 < options.array_combination_threshold as usize
783                            && (!known_items_inner.keys().any(|k| combination.keyed_array_entries.contains_key(k))
784                                || shapes_are_discriminated(
785                                    known_items_inner,
786                                    &combination.keyed_array_entries,
787                                    codebase,
788                                ))
789                        {
790                            let frozen = TArray::Keyed(TKeyedArray {
791                                known_items: Some(std::mem::take(&mut combination.keyed_array_entries)),
792                                parameters: None,
793                                non_empty: combination.flags.contains(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED),
794                            });
795                            combination.sealed_arrays.push(frozen);
796                            combination.sealed_arrays.push(TArray::Keyed(TKeyedArray {
797                                known_items,
798                                parameters,
799                                non_empty,
800                            }));
801                            combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
802                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
803                            combination.flags.insert(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
804
805                            continue;
806                        }
807                    }
808
809                    combination.flags.insert(CombinationFlags::HAS_KEYED_ARRAY);
810
811                    if non_empty {
812                        combination.flags.insert(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
813                    } else {
814                        combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
815
816                        if parameters.is_none()
817                            && known_items.as_ref().is_none_or(|items| items.is_empty())
818                            && combination.list_array_parameter.is_some()
819                        {
820                            combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
821                            for (is_optional, _) in combination.list_array_entries.values_mut() {
822                                *is_optional = true;
823                            }
824
825                            had_previous_keyed_array = false;
826                            combination.flags.remove(CombinationFlags::HAS_KEYED_ARRAY);
827
828                            continue;
829                        }
830                    }
831
832                    if let Some(known_items) = known_items {
833                        let has_existing_entries =
834                            !combination.keyed_array_entries.is_empty() || had_previous_keyed_array;
835                        let mut possibly_undefined_entries =
836                            combination.keyed_array_entries.keys().copied().collect::<HashSet<_>>();
837
838                        let mut has_defined_keys = false;
839
840                        for (candidate_item_name, (cu, candidate_item_type)) in known_items {
841                            if let Some((eu, existing_type)) =
842                                combination.keyed_array_entries.get_mut(&candidate_item_name)
843                            {
844                                if cu {
845                                    *eu = true;
846                                }
847                                if &candidate_item_type != existing_type {
848                                    *existing_type =
849                                        combine_union_types(existing_type, &candidate_item_type, codebase, options);
850                                }
851                            } else {
852                                let new_item_value_type =
853                                    if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
854                                        combination.keyed_array_parameters
855                                    {
856                                        adjust_keyed_array_parameters(
857                                            existing_value_param,
858                                            &candidate_item_type,
859                                            codebase,
860                                            options,
861                                            &candidate_item_name,
862                                            existing_key_param,
863                                        );
864
865                                        continue;
866                                    } else {
867                                        let new_type = candidate_item_type.clone();
868                                        (has_existing_entries || cu, new_type)
869                                    };
870
871                                combination.keyed_array_entries.insert(candidate_item_name, new_item_value_type);
872                            }
873
874                            possibly_undefined_entries.remove(&candidate_item_name);
875
876                            if !cu {
877                                has_defined_keys = true;
878                            }
879                        }
880
881                        if !has_defined_keys {
882                            combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
883                        }
884
885                        for possibly_undefined_type_key in possibly_undefined_entries {
886                            let possibly_undefined_type =
887                                combination.keyed_array_entries.get_mut(&possibly_undefined_type_key);
888                            if let Some((pu, _)) = possibly_undefined_type {
889                                *pu = true;
890                            }
891                        }
892                    } else if !options.overwrite_empty_array {
893                        if match &parameters {
894                            Some((_, value_param)) => value_param.is_never(),
895                            None => true,
896                        } {
897                            for (tu, _) in combination.keyed_array_entries.values_mut() {
898                                *tu = true;
899                            }
900                        } else {
901                            for (key, (_, entry_type)) in &combination.keyed_array_entries {
902                                if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
903                                    combination.keyed_array_parameters
904                                {
905                                    adjust_keyed_array_parameters(
906                                        existing_value_param,
907                                        entry_type,
908                                        codebase,
909                                        options,
910                                        key,
911                                        existing_key_param,
912                                    );
913                                }
914                            }
915
916                            combination.keyed_array_entries.clear();
917                        }
918                    }
919
920                    combination.keyed_array_parameters = match (&combination.keyed_array_parameters, parameters) {
921                        (None, None) => None,
922                        (Some(existing_types), None) => Some(existing_types.clone()),
923                        (None, Some(params)) => Some(((*params.0).clone(), (*params.1).clone())),
924                        (Some(existing_types), Some(params)) => Some((
925                            combine_union_types(&existing_types.0, &params.0, codebase, options),
926                            combine_union_types(&existing_types.1, &params.1, codebase, options),
927                        )),
928                    };
929                }
930            }
931        }
932
933        return;
934    }
935
936    // this probably won't ever happen, but the object top type
937    // can eliminate variants
938    if atomic == TAtomic::Object(TObject::Any) {
939        combination.flags.insert(CombinationFlags::HAS_OBJECT_TOP_TYPE);
940        combination.value_types.retain(|_, t| !matches!(t, TAtomic::Object(TObject::Named(_))));
941        combination.value_types.insert(atomic.get_id(), atomic);
942
943        return;
944    }
945
946    if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
947        if let Some(object_static) = combination.object_static.get(&named_object.get_name()) {
948            if *object_static && !named_object.is_static {
949                combination.object_static.insert(named_object.get_name(), false);
950            }
951        } else {
952            combination.object_static.insert(named_object.get_name(), named_object.is_static);
953        }
954    }
955
956    if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
957        let fq_class_name = named_object.get_name();
958        if let Some(type_parameters) = named_object.get_type_parameters() {
959            let object_type_key = get_combiner_key(fq_class_name, type_parameters, codebase);
960
961            if let Some((_, existing_type_params)) = combination.object_type_params.get(&object_type_key) {
962                let mut new_type_parameters = Vec::with_capacity(type_parameters.len());
963                for (i, type_param) in type_parameters.iter().enumerate() {
964                    if let Some(existing_type_param) = existing_type_params.get(i) {
965                        new_type_parameters.push(combine_union_types(
966                            existing_type_param,
967                            type_param,
968                            codebase,
969                            options,
970                        ));
971                    }
972                }
973
974                combination.object_type_params.insert(object_type_key, (fq_class_name, new_type_parameters));
975            } else {
976                combination.object_type_params.insert(object_type_key, (fq_class_name, type_parameters.to_vec()));
977            }
978
979            return;
980        }
981    }
982
983    if let TAtomic::Object(TObject::Enum(enum_object)) = atomic {
984        combination.enum_names.insert((enum_object.get_name(), enum_object.get_case()));
985
986        return;
987    }
988
989    if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
990        let fq_class_name = named_object.get_name();
991        let intersection_types = named_object.get_intersection_types();
992
993        if combination.flags.contains(CombinationFlags::HAS_OBJECT_TOP_TYPE)
994            || combination.value_types.contains_key(&atomic.get_id())
995        {
996            return;
997        }
998
999        let Some(symbol_type) = codebase.symbols.get_kind(fq_class_name) else {
1000            combination.value_types.insert(atomic.get_id(), atomic);
1001            return;
1002        };
1003
1004        if !matches!(symbol_type, SymbolKind::Class | SymbolKind::Enum | SymbolKind::Interface) {
1005            combination.value_types.insert(atomic.get_id(), atomic);
1006            return;
1007        }
1008
1009        let is_class = matches!(symbol_type, SymbolKind::Class);
1010        let is_interface = matches!(symbol_type, SymbolKind::Interface);
1011
1012        let mut types_to_remove: Vec<Word> = Vec::new();
1013
1014        for (key, existing_type) in &combination.value_types {
1015            if let TAtomic::Object(TObject::Named(existing_object)) = &existing_type {
1016                let existing_name = existing_object.get_name();
1017
1018                if intersection_types.is_some() || existing_object.has_intersection_types() {
1019                    if object_comparator::is_shallowly_contained_by(
1020                        codebase,
1021                        existing_type,
1022                        &atomic,
1023                        false,
1024                        &mut ComparisonResult::new(),
1025                    ) {
1026                        types_to_remove.push(existing_name);
1027                        continue;
1028                    }
1029
1030                    if object_comparator::is_shallowly_contained_by(
1031                        codebase,
1032                        &atomic,
1033                        existing_type,
1034                        false,
1035                        &mut ComparisonResult::new(),
1036                    ) {
1037                        return;
1038                    }
1039
1040                    continue;
1041                }
1042
1043                let Some(existing_symbol_kind) = codebase.symbols.get_kind(existing_object.get_name()) else {
1044                    continue;
1045                };
1046
1047                if matches!(existing_symbol_kind, SymbolKind::Class) {
1048                    // remove subclasses
1049                    if codebase.is_instance_of(existing_name.as_bytes(), fq_class_name.as_bytes()) {
1050                        types_to_remove.push(*key);
1051                        continue;
1052                    }
1053
1054                    if is_class {
1055                        // if covered by a parent class
1056                        if codebase.class_extends(fq_class_name.as_bytes(), existing_name.as_bytes()) {
1057                            return;
1058                        }
1059                    } else if is_interface {
1060                        // if covered by a parent class
1061                        if codebase.class_implements(fq_class_name.as_bytes(), existing_name.as_bytes()) {
1062                            return;
1063                        }
1064                    }
1065                } else if matches!(existing_symbol_kind, SymbolKind::Interface) {
1066                    if codebase.class_implements(existing_name.as_bytes(), fq_class_name.as_bytes()) {
1067                        types_to_remove.push(existing_name);
1068                        continue;
1069                    }
1070
1071                    if (is_class || is_interface)
1072                        && codebase.class_implements(fq_class_name.as_bytes(), existing_name.as_bytes())
1073                    {
1074                        return;
1075                    }
1076                }
1077            }
1078        }
1079
1080        combination.value_types.insert(atomic.get_id(), atomic);
1081
1082        for type_key in types_to_remove {
1083            combination.value_types.remove(&type_key);
1084        }
1085
1086        return;
1087    }
1088
1089    if atomic == TAtomic::Scalar(TScalar::Generic) {
1090        combination.literal_strings.clear();
1091        combination.integers.clear();
1092        combination.literal_floats.clear();
1093        combination.value_types.retain(|k, _| {
1094            k.as_bytes() != b"string"
1095                && k.as_bytes() != b"bool"
1096                && k.as_bytes() != b"false"
1097                && k.as_bytes() != b"true"
1098                && k.as_bytes() != b"float"
1099                && k.as_bytes() != b"numeric"
1100                && k.as_bytes() != b"array-key"
1101        });
1102
1103        combination.value_types.insert(atomic.get_id(), atomic);
1104        return;
1105    }
1106
1107    if atomic == TAtomic::Scalar(TScalar::ArrayKey) {
1108        if combination.value_types.contains_key(&*ATOM_SCALAR) {
1109            return;
1110        }
1111
1112        combination.literal_strings.clear();
1113        combination.integers.clear();
1114        combination.value_types.retain(|k, _| k != &*ATOM_STRING && k != &*ATOM_INT);
1115        combination.value_types.insert(atomic.get_id(), atomic);
1116
1117        return;
1118    }
1119
1120    if let TAtomic::Scalar(TScalar::String(_) | TScalar::Integer(_)) = atomic
1121        && (combination.value_types.contains_key(&*ATOM_SCALAR)
1122            || combination.value_types.contains_key(&*ATOM_ARRAY_KEY))
1123    {
1124        return;
1125    }
1126
1127    if let TAtomic::Scalar(TScalar::Float(_) | TScalar::Integer(_)) = atomic
1128        && (combination.value_types.contains_key(&*ATOM_NUMERIC) || combination.value_types.contains_key(&*ATOM_SCALAR))
1129    {
1130        return;
1131    }
1132
1133    if let TAtomic::Scalar(TScalar::String(mut string_scalar)) = atomic {
1134        if let Some(existing_string_type) = combination.value_types.get_mut(&*ATOM_STRING) {
1135            if let TAtomic::Scalar(TScalar::String(existing_string_type)) = existing_string_type {
1136                if let Some(lit_atom) = string_scalar.get_known_literal_atom() {
1137                    let lit_value = lit_atom.as_bytes();
1138                    let is_incompatible = (existing_string_type.is_numeric && !str_is_numeric(lit_value))
1139                        || (existing_string_type.is_truthy && (lit_value.is_empty() || lit_value == b"0"))
1140                        || (existing_string_type.is_non_empty && lit_value.is_empty())
1141                        || (existing_string_type.is_lowercase() && lit_value.iter().any(u8::is_ascii_uppercase))
1142                        || (existing_string_type.is_uppercase() && lit_value.iter().any(u8::is_ascii_lowercase));
1143
1144                    if is_incompatible {
1145                        // Check threshold before adding literal string
1146                        if combination.literal_strings.len() >= options.string_combination_threshold as usize {
1147                            // Exceeded threshold - just merge into the base string type
1148                            *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1149                        } else {
1150                            combination.literal_strings.insert(lit_atom);
1151                        }
1152                    } else {
1153                        *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1154                    }
1155                } else {
1156                    *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1157                }
1158            }
1159        } else if let Some(atom) = string_scalar.get_known_literal_atom() {
1160            // Check threshold before adding literal string
1161            if combination.literal_strings.len() >= options.string_combination_threshold as usize {
1162                // Exceeded threshold - generalize to base string type
1163                combination.literal_strings.clear();
1164                combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::string()));
1165            } else {
1166                combination.literal_strings.insert(atom);
1167            }
1168        } else {
1169            let mut literals_to_keep = WordSet::default();
1170            if !combination.literal_strings.is_empty() {
1171                string_scalar.is_callable = false;
1172            }
1173
1174            if string_scalar.is_truthy
1175                || string_scalar.is_non_empty
1176                || string_scalar.is_numeric
1177                || !string_scalar.casing.is_unspecified()
1178            {
1179                for value in &combination.literal_strings {
1180                    if value.is_empty() {
1181                        string_scalar.is_non_empty = false;
1182                        string_scalar.is_truthy = false;
1183                        string_scalar.is_numeric = false;
1184                        break;
1185                    } else if value.as_bytes() == b"0" {
1186                        string_scalar.is_truthy = false;
1187                    }
1188
1189                    if string_scalar.is_numeric && !str_is_numeric(value.as_bytes()) {
1190                        literals_to_keep.insert(*value);
1191                    } else {
1192                        string_scalar.is_numeric = string_scalar.is_numeric && str_is_numeric(value.as_bytes());
1193                    }
1194
1195                    string_scalar.casing = match string_scalar.casing {
1196                        TStringCasing::Lowercase if value.as_bytes().iter().all(u8::is_ascii_lowercase) => {
1197                            TStringCasing::Lowercase
1198                        }
1199                        TStringCasing::Uppercase if value.as_bytes().iter().all(u8::is_ascii_uppercase) => {
1200                            TStringCasing::Uppercase
1201                        }
1202                        _ => TStringCasing::Unspecified,
1203                    };
1204                }
1205            }
1206
1207            combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::String(string_scalar)));
1208
1209            std::mem::swap(&mut combination.literal_strings, &mut literals_to_keep);
1210        }
1211
1212        return;
1213    }
1214
1215    if let TAtomic::Scalar(TScalar::Integer(integer)) = &atomic {
1216        // If we already have the base int type, no need to track literals
1217        if combination.value_types.contains_key(&*ATOM_INT) {
1218            return;
1219        }
1220
1221        // Check if adding this integer would exceed the threshold
1222        if integer.is_literal() && combination.integers.len() >= options.integer_combination_threshold as usize {
1223            // Exceeded threshold - generalize to base int type
1224            combination.integers.clear();
1225            combination.value_types.insert(*ATOM_INT, TAtomic::Scalar(TScalar::int()));
1226            return;
1227        }
1228
1229        combination.integers.push(*integer);
1230
1231        return;
1232    }
1233
1234    if let TAtomic::Scalar(TScalar::Float(float_scalar)) = &atomic {
1235        if let Some(stored) = combination.value_types.get(&*ATOM_FLOAT) {
1236            if matches!(stored, TAtomic::Scalar(TScalar::Float(TFloat::Float))) {
1237                return;
1238            }
1239
1240            if matches!(float_scalar, TFloat::Float) {
1241                combination.literal_floats.clear();
1242                combination.value_types.insert(*ATOM_FLOAT, atomic);
1243            }
1244
1245            return;
1246        }
1247
1248        if let TFloat::Literal(literal_value) = float_scalar {
1249            if combination.literal_floats.len() >= options.string_combination_threshold as usize {
1250                combination.literal_floats.clear();
1251                combination.value_types.insert(*ATOM_FLOAT, TAtomic::Scalar(TScalar::float()));
1252                return;
1253            }
1254            combination.literal_floats.push(*literal_value);
1255        } else {
1256            combination.literal_floats.clear();
1257            combination.value_types.insert(*ATOM_FLOAT, atomic);
1258        }
1259
1260        return;
1261    }
1262
1263    combination.value_types.insert(atomic.get_id(), atomic);
1264}
1265
1266fn shapes_are_discriminated(
1267    incoming: &BTreeMap<ArrayKey, (bool, TUnion)>,
1268    existing: &BTreeMap<ArrayKey, (bool, TUnion)>,
1269    codebase: &CodebaseMetadata,
1270) -> bool {
1271    let mut has_asymmetric_keys = false;
1272    for key in incoming.keys() {
1273        if !existing.contains_key(key) {
1274            has_asymmetric_keys = true;
1275            break;
1276        }
1277    }
1278
1279    if !has_asymmetric_keys {
1280        for key in existing.keys() {
1281            if !incoming.contains_key(key) {
1282                has_asymmetric_keys = true;
1283                break;
1284            }
1285        }
1286    }
1287
1288    if !has_asymmetric_keys {
1289        return false;
1290    }
1291
1292    for (key, (incoming_optional, incoming_type)) in incoming {
1293        if *incoming_optional {
1294            continue;
1295        }
1296
1297        let Some((existing_optional, existing_type)) = existing.get(key) else {
1298            continue;
1299        };
1300
1301        if *existing_optional {
1302            continue;
1303        }
1304
1305        if !union_comparator::can_expression_types_be_identical(codebase, incoming_type, existing_type, false, false) {
1306            return true;
1307        }
1308    }
1309
1310    false
1311}
1312
1313/// Widens known items in a sealed array with the generic value type from parameters.
1314/// This is needed when combining a sealed array with a parametric one, the parametric
1315/// array's generic string keys could overwrite any of the sealed array's known keys.
1316fn widen_known_items_with_params(
1317    known_items: Option<BTreeMap<ArrayKey, (bool, TUnion)>>,
1318    params: Option<&(TUnion, TUnion)>,
1319    other_known_items: &BTreeMap<ArrayKey, (bool, TUnion)>,
1320    codebase: &CodebaseMetadata,
1321    options: CombinerOptions,
1322) -> Option<BTreeMap<ArrayKey, (bool, TUnion)>> {
1323    let mut items = known_items?;
1324
1325    if let Some((key_param, value_param)) = params {
1326        let (key_param_accepts_int, key_param_accepts_string) =
1327            if key_param.has_mixed() || key_param.has_mixed_template() {
1328                (true, true)
1329            } else {
1330                let mut accepts_int = false;
1331                let mut accepts_string = false;
1332                for part in key_param.types.as_ref() {
1333                    if accepts_int && accepts_string {
1334                        break;
1335                    }
1336
1337                    match part {
1338                        TAtomic::Scalar(TScalar::ArrayKey) => {
1339                            accepts_int = true;
1340                            accepts_string = true;
1341                        }
1342                        TAtomic::Scalar(TScalar::Integer(_)) => accepts_int = true,
1343                        TAtomic::Scalar(TScalar::String(_)) => accepts_string = true,
1344                        _ => {
1345                            accepts_int = true;
1346                            accepts_string = true;
1347                        }
1348                    }
1349                }
1350
1351                (accepts_int, accepts_string)
1352            };
1353
1354        if !key_param_accepts_int && !key_param_accepts_string {
1355            return Some(items);
1356        }
1357
1358        for (key, (_, entry_type)) in items.iter_mut() {
1359            if entry_type == value_param {
1360                continue;
1361            }
1362
1363            if other_known_items.contains_key(key) {
1364                continue;
1365            }
1366
1367            let key_compatible = match key {
1368                ArrayKey::Integer(_) => key_param_accepts_int,
1369                ArrayKey::String(_) => key_param_accepts_string,
1370                ArrayKey::ClassLikeConstant { .. } => key_param_accepts_int || key_param_accepts_string,
1371            };
1372
1373            if !key_compatible {
1374                continue;
1375            }
1376
1377            *entry_type = combine_union_types(entry_type, value_param, codebase, options);
1378        }
1379    }
1380
1381    Some(items)
1382}
1383
1384fn adjust_keyed_array_parameters(
1385    existing_value_param: &mut TUnion,
1386    entry_type: &TUnion,
1387    codebase: &CodebaseMetadata,
1388    options: CombinerOptions,
1389    key: &ArrayKey,
1390    existing_key_param: &mut TUnion,
1391) {
1392    *existing_value_param = combine_union_types(existing_value_param, entry_type, codebase, options);
1393    let new_key_type = key.to_union();
1394    *existing_key_param = combine_union_types(existing_key_param, &new_key_type, codebase, options);
1395}
1396
1397fn flush_sealed_keyed_arrays_into_combination(
1398    combination: &mut TypeCombination,
1399    codebase: &CodebaseMetadata,
1400    options: CombinerOptions,
1401) {
1402    let sealed = std::mem::take(&mut combination.sealed_arrays);
1403    let mut any_keyed = false;
1404    let mut put_back = Vec::new();
1405
1406    for array in sealed {
1407        let TArray::Keyed(keyed) = array else {
1408            put_back.push(array);
1409            continue;
1410        };
1411
1412        any_keyed = true;
1413        let TKeyedArray { known_items, parameters, non_empty } = keyed;
1414
1415        if non_empty {
1416            combination.flags.insert(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
1417        } else {
1418            combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
1419        }
1420
1421        if let Some(known_items) = known_items {
1422            for (candidate_item_name, (candidate_optional, candidate_item_type)) in known_items {
1423                if let Some((existing_optional, existing_type)) =
1424                    combination.keyed_array_entries.get_mut(&candidate_item_name)
1425                {
1426                    if candidate_optional {
1427                        *existing_optional = true;
1428                    }
1429                    if &candidate_item_type != existing_type {
1430                        *existing_type = combine_union_types(existing_type, &candidate_item_type, codebase, options);
1431                    }
1432                } else {
1433                    let inserted = if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
1434                        combination.keyed_array_parameters
1435                    {
1436                        adjust_keyed_array_parameters(
1437                            existing_value_param,
1438                            &candidate_item_type,
1439                            codebase,
1440                            options,
1441                            &candidate_item_name,
1442                            existing_key_param,
1443                        );
1444                        None
1445                    } else {
1446                        Some((true, candidate_item_type.clone()))
1447                    };
1448
1449                    if let Some(entry) = inserted {
1450                        combination.keyed_array_entries.insert(candidate_item_name, entry);
1451                    }
1452                }
1453            }
1454        }
1455
1456        combination.keyed_array_parameters = match (combination.keyed_array_parameters.take(), parameters) {
1457            (None, None) => None,
1458            (Some(existing_types), None) => Some(existing_types),
1459            (None, Some(params)) => Some(((*params.0).clone(), (*params.1).clone())),
1460            (Some(existing_types), Some(params)) => Some((
1461                combine_union_types(&existing_types.0, &params.0, codebase, options),
1462                combine_union_types(&existing_types.1, &params.1, codebase, options),
1463            )),
1464        };
1465    }
1466
1467    if any_keyed {
1468        combination.flags.insert(CombinationFlags::HAS_KEYED_ARRAY);
1469    }
1470
1471    combination.sealed_arrays = put_back;
1472}
1473
1474const COMBINER_KEY_STACK_BUF: usize = 256;
1475
1476fn get_combiner_key(name: Word, type_params: &[TUnion], codebase: &CodebaseMetadata) -> Word {
1477    let covariants = if let Some(class_like_metadata) = codebase.get_class_like(name.as_bytes()) {
1478        &class_like_metadata.template_variance
1479    } else {
1480        return name;
1481    };
1482
1483    let name_str = name.as_bytes();
1484    let mut estimated_len = name_str.len() + 2; // name + "<" + ">"
1485    for (i, tunion) in type_params.iter().enumerate() {
1486        if i > 0 {
1487            estimated_len += 2; // ", "
1488        }
1489
1490        if covariants.get(i) == Some(&Variance::Covariant) {
1491            estimated_len += 1; // "*"
1492        } else {
1493            estimated_len += tunion.get_id().len();
1494        }
1495    }
1496
1497    if estimated_len <= COMBINER_KEY_STACK_BUF {
1498        let mut buffer = [0u8; COMBINER_KEY_STACK_BUF];
1499        let mut pos = 0;
1500
1501        buffer[pos..pos + name_str.len()].copy_from_slice(name_str);
1502        pos += name_str.len();
1503
1504        buffer[pos] = b'<';
1505        pos += 1;
1506
1507        for (i, tunion) in type_params.iter().enumerate() {
1508            if i > 0 {
1509                buffer[pos..pos + 2].copy_from_slice(b", ");
1510                pos += 2;
1511            }
1512            let id_word = tunion.get_id();
1513            let param_bytes: &[u8] =
1514                if covariants.get(i) == Some(&Variance::Covariant) { b"*" } else { id_word.as_bytes() };
1515            let need = param_bytes.len();
1516            buffer[pos..pos + need].copy_from_slice(param_bytes);
1517            pos += need;
1518        }
1519
1520        buffer[pos] = b'>';
1521        pos += 1;
1522
1523        return word(&buffer[..pos]);
1524    }
1525
1526    let mut result: Vec<u8> = Vec::with_capacity(estimated_len);
1527    result.extend_from_slice(name_str);
1528    result.push(b'<');
1529    for (i, tunion) in type_params.iter().enumerate() {
1530        if i > 0 {
1531            result.extend_from_slice(b", ");
1532        }
1533        if covariants.get(i) == Some(&Variance::Covariant) {
1534            result.push(b'*');
1535        } else {
1536            result.extend_from_slice(tunion.get_id().as_bytes());
1537        }
1538    }
1539    result.push(b'>');
1540    word(&result)
1541}
1542
1543fn combine_string_scalars(s1: &TString, s2: TString) -> TString {
1544    TString {
1545        literal: match (&s1.literal, s2.literal) {
1546            (Some(TStringLiteral::Value(v1)), Some(TStringLiteral::Value(v2))) => {
1547                if v1 == &v2 {
1548                    Some(TStringLiteral::Value(v2))
1549                } else {
1550                    Some(TStringLiteral::Unspecified)
1551                }
1552            }
1553            (Some(TStringLiteral::Unspecified), Some(_)) | (Some(_), Some(TStringLiteral::Unspecified)) => {
1554                Some(TStringLiteral::Unspecified)
1555            }
1556            _ => None,
1557        },
1558        is_numeric: s1.is_numeric && s2.is_numeric,
1559        is_truthy: s1.is_truthy && s2.is_truthy,
1560        is_non_empty: s1.is_non_empty && s2.is_non_empty,
1561        is_callable: s1.is_callable && s2.is_callable,
1562        casing: match (s1.casing, s2.casing) {
1563            (TStringCasing::Lowercase, TStringCasing::Lowercase) => TStringCasing::Lowercase,
1564            (TStringCasing::Uppercase, TStringCasing::Uppercase) => TStringCasing::Uppercase,
1565            _ => TStringCasing::Unspecified,
1566        },
1567    }
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572    use std::collections::BTreeMap;
1573
1574    use super::*;
1575
1576    use crate::ttype::atomic::TAtomic;
1577    use crate::ttype::atomic::array::list::TList;
1578    use crate::ttype::atomic::scalar::TScalar;
1579
1580    #[test]
1581    fn test_combine_scalars() {
1582        let types = vec![
1583            TAtomic::Scalar(TScalar::string()),
1584            TAtomic::Scalar(TScalar::int()),
1585            TAtomic::Scalar(TScalar::float()),
1586            TAtomic::Scalar(TScalar::bool()),
1587        ];
1588
1589        let combined =
1590            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1591
1592        assert_eq!(combined.len(), 1);
1593        assert!(matches!(combined[0], TAtomic::Scalar(TScalar::Generic)));
1594    }
1595
1596    #[test]
1597    fn test_combine_boolean_lists() {
1598        let types = vec![
1599            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1600                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#false())))),
1601                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#true())))),
1602            ])))),
1603            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1604                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#true())))),
1605                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#false())))),
1606            ])))),
1607        ];
1608
1609        let combined =
1610            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1611
1612        assert_eq!(combined.len(), 2);
1613        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1614        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1615    }
1616
1617    #[test]
1618    fn test_combine_integer_lists() {
1619        let types = vec![
1620            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1621                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1622                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1623            ])))),
1624            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1625                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1626                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1627            ])))),
1628        ];
1629
1630        let combined =
1631            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1632
1633        assert_eq!(combined.len(), 2);
1634        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1635        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1636    }
1637
1638    #[test]
1639    fn test_combine_string_lists() {
1640        let types = vec![
1641            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1642                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1643                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1644            ])))),
1645            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1646                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1647                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1648            ])))),
1649        ];
1650
1651        let combined =
1652            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1653
1654        assert_eq!(combined.len(), 2);
1655        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1656        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1657    }
1658
1659    #[test]
1660    fn test_combine_mixed_literal_lists() {
1661        let types = vec![
1662            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1663                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1664                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1665            ])))),
1666            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1667                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1668                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1669            ])))),
1670        ];
1671
1672        let combined =
1673            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1674
1675        assert_eq!(combined.len(), 2);
1676        assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1677        assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1678    }
1679
1680    #[test]
1681    fn test_combine_list_with_generic_list() {
1682        let types = vec![
1683            TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1684                (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1685                (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1686            ])))),
1687            TAtomic::Array(TArray::List(TList::new(Arc::new(TUnion::from_atomic(TAtomic::Scalar(TScalar::int())))))), // list<int>
1688        ];
1689
1690        let combined =
1691            combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1692
1693        // Expecting list{1,2} and list<int> = list<int>
1694        assert_eq!(combined.len(), 1);
1695
1696        let TAtomic::Array(TArray::List(list_type)) = &combined[0] else {
1697            panic!("Expected a list type");
1698        };
1699
1700        let Some(known_elements) = &list_type.known_elements else {
1701            panic!("Expected known elements");
1702        };
1703
1704        assert!(!list_type.is_non_empty());
1705        assert!(list_type.known_count.is_none());
1706        assert!(list_type.element_type.is_int());
1707
1708        assert_eq!(known_elements.len(), 2);
1709        assert!(known_elements.contains_key(&0));
1710        assert!(known_elements.contains_key(&1));
1711
1712        let Some(first_element) = known_elements.get(&0) else {
1713            panic!("Expected first element");
1714        };
1715
1716        let Some(second_element) = known_elements.get(&1) else {
1717            panic!("Expected second element");
1718        };
1719
1720        assert!(first_element.1.is_int());
1721        assert!(second_element.1.is_int());
1722    }
1723}