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