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