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