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