1use std::sync::LazyLock;
2
3use ahash::HashSet;
4
5use mago_atom::Atom;
6use mago_atom::atom;
7
8static ATOM_FALSE: LazyLock<Atom> = LazyLock::new(|| atom("false"));
9static ATOM_TRUE: LazyLock<Atom> = LazyLock::new(|| atom("true"));
10static ATOM_BOOL: LazyLock<Atom> = LazyLock::new(|| atom("bool"));
11static ATOM_VOID: LazyLock<Atom> = LazyLock::new(|| atom("void"));
12static ATOM_NULL: LazyLock<Atom> = LazyLock::new(|| atom("null"));
13static ATOM_STRING: LazyLock<Atom> = LazyLock::new(|| atom("string"));
14static ATOM_FLOAT: LazyLock<Atom> = LazyLock::new(|| atom("float"));
15static ATOM_INT: LazyLock<Atom> = LazyLock::new(|| atom("int"));
16static ATOM_MIXED: LazyLock<Atom> = LazyLock::new(|| atom("mixed"));
17static ATOM_SCALAR: LazyLock<Atom> = LazyLock::new(|| atom("scalar"));
18static ATOM_ARRAY_KEY: LazyLock<Atom> = LazyLock::new(|| atom("array-key"));
19static ATOM_NUMERIC: LazyLock<Atom> = LazyLock::new(|| atom("numeric"));
20static ATOM_NEVER: LazyLock<Atom> = LazyLock::new(|| atom("never"));
21
22use crate::metadata::CodebaseMetadata;
23use crate::symbol::SymbolKind;
24use crate::ttype::TType;
25use crate::ttype::atomic::TAtomic;
26use crate::ttype::atomic::array::TArray;
27use crate::ttype::atomic::array::key::ArrayKey;
28use crate::ttype::atomic::array::keyed::TKeyedArray;
29use crate::ttype::atomic::array::list::TList;
30use crate::ttype::atomic::mixed::TMixed;
31use crate::ttype::atomic::mixed::truthiness::TMixedTruthiness;
32use crate::ttype::atomic::object::TObject;
33use crate::ttype::atomic::object::named::TNamedObject;
34use crate::ttype::atomic::resource::TResource;
35use crate::ttype::atomic::scalar::TScalar;
36use crate::ttype::atomic::scalar::float::TFloat;
37use crate::ttype::atomic::scalar::int::TInteger;
38use crate::ttype::atomic::scalar::string::TString;
39use crate::ttype::atomic::scalar::string::TStringLiteral;
40use crate::ttype::combination::CombinationFlags;
41use crate::ttype::combination::TypeCombination;
42use crate::ttype::combine_union_types;
43use crate::ttype::comparator::ComparisonResult;
44use crate::ttype::comparator::array_comparator::is_array_contained_by_array;
45use crate::ttype::comparator::object_comparator;
46use crate::ttype::template::variance::Variance;
47use crate::ttype::union::TUnion;
48use crate::utils::str_is_numeric;
49
50pub const DEFAULT_ARRAY_COMBINATION_THRESHOLD: u16 = 128;
57
58pub const DEFAULT_STRING_COMBINATION_THRESHOLD: u16 = 128;
64
65pub const DEFAULT_INTEGER_COMBINATION_THRESHOLD: u16 = 128;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct CombinerOptions {
75 pub overwrite_empty_array: bool,
77 pub array_combination_threshold: u16,
79 pub string_combination_threshold: u16,
81 pub integer_combination_threshold: u16,
83}
84
85impl Default for CombinerOptions {
86 fn default() -> Self {
87 Self {
88 overwrite_empty_array: false,
89 array_combination_threshold: DEFAULT_ARRAY_COMBINATION_THRESHOLD,
90 string_combination_threshold: DEFAULT_STRING_COMBINATION_THRESHOLD,
91 integer_combination_threshold: DEFAULT_INTEGER_COMBINATION_THRESHOLD,
92 }
93 }
94}
95
96impl CombinerOptions {
97 #[inline]
99 #[must_use]
100 pub fn with_overwrite_empty_array(mut self) -> Self {
101 self.overwrite_empty_array = true;
102 self
103 }
104
105 #[inline]
107 #[must_use]
108 pub fn with_array_combination_threshold(mut self, threshold: u16) -> Self {
109 self.array_combination_threshold = threshold;
110 self
111 }
112
113 #[inline]
115 #[must_use]
116 pub fn with_string_combination_threshold(mut self, threshold: u16) -> Self {
117 self.string_combination_threshold = threshold;
118 self
119 }
120
121 #[inline]
123 #[must_use]
124 pub fn with_integer_combination_threshold(mut self, threshold: u16) -> Self {
125 self.integer_combination_threshold = threshold;
126 self
127 }
128}
129
130pub fn combine(types: Vec<TAtomic>, codebase: &CodebaseMetadata, options: CombinerOptions) -> Vec<TAtomic> {
131 if types.len() == 1 {
132 return types;
133 }
134
135 let mut combination = TypeCombination::new();
136
137 for atomic in types {
138 if let TAtomic::Derived(derived) = atomic {
139 combination.derived_types.insert(derived);
140 continue;
141 }
142
143 scrape_type_properties(atomic, &mut combination, codebase, options);
144 }
145
146 combination.integers.sort_unstable();
147 combination.integers.dedup();
148 combination.literal_floats.sort_unstable();
149 combination.literal_floats.dedup();
150
151 finalize_sealed_arrays(&mut combination.sealed_arrays, codebase);
152
153 let is_falsy_mixed = combination.flags.falsy_mixed().unwrap_or(false);
154 let is_truthy_mixed = combination.flags.truthy_mixed().unwrap_or(false);
155 let is_nonnull_mixed = combination.flags.nonnull_mixed().unwrap_or(false);
156
157 if is_falsy_mixed
158 || is_nonnull_mixed
159 || combination.flags.contains(CombinationFlags::GENERIC_MIXED)
160 || is_truthy_mixed
161 {
162 return vec![TAtomic::Mixed(TMixed::new().with_is_non_null(is_nonnull_mixed).with_truthiness(
163 if is_truthy_mixed && !is_falsy_mixed {
164 TMixedTruthiness::Truthy
165 } else if is_falsy_mixed && !is_truthy_mixed {
166 TMixedTruthiness::Falsy
167 } else {
168 TMixedTruthiness::Undetermined
169 },
170 ))];
171 } else if combination.flags.contains(CombinationFlags::HAS_MIXED) {
172 return vec![TAtomic::Mixed(TMixed::new())];
173 }
174
175 if combination.is_simple() {
176 if combination.value_types.contains_key(&*ATOM_FALSE) {
177 return vec![TAtomic::Scalar(TScalar::r#false())];
178 }
179
180 if combination.value_types.contains_key(&*ATOM_TRUE) {
181 return vec![TAtomic::Scalar(TScalar::r#true())];
182 }
183
184 return combination.value_types.into_values().collect();
185 }
186
187 if combination.value_types.remove(&*ATOM_VOID).is_some() && combination.value_types.contains_key(&*ATOM_NULL) {
188 combination.value_types.insert(*ATOM_NULL, TAtomic::Null);
189 }
190
191 if combination.value_types.contains_key(&*ATOM_FALSE) && combination.value_types.contains_key(&*ATOM_TRUE) {
192 combination.value_types.remove(&*ATOM_FALSE);
193 combination.value_types.remove(&*ATOM_TRUE);
194 combination.value_types.insert(*ATOM_BOOL, TAtomic::Scalar(TScalar::bool()));
195 }
196
197 let estimated_capacity = combination.derived_types.len()
198 + combination.integers.len().min(10)
199 + combination.literal_floats.len()
200 + combination.enum_names.len()
201 + combination.value_types.len()
202 + combination.sealed_arrays.len()
203 + 5;
204
205 let mut new_types = Vec::with_capacity(estimated_capacity);
206 for derived_type in combination.derived_types {
207 new_types.push(TAtomic::Derived(derived_type));
208 }
209
210 if combination.flags.contains(CombinationFlags::RESOURCE) {
211 new_types.push(TAtomic::Resource(TResource { closed: None }));
212 } else {
213 let open = combination.flags.contains(CombinationFlags::OPEN_RESOURCE);
214 let closed = combination.flags.contains(CombinationFlags::CLOSED_RESOURCE);
215 match (open, closed) {
216 (true, true) => {
217 new_types.push(TAtomic::Resource(TResource { closed: None }));
218 }
219 (true, false) => {
220 new_types.push(TAtomic::Resource(TResource { closed: Some(false) }));
221 }
222 (false, true) => {
223 new_types.push(TAtomic::Resource(TResource { closed: Some(true) }));
224 }
225 _ => {
226 }
228 }
229 }
230
231 let mut arrays = vec![];
232
233 if combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY) {
234 arrays.push(TArray::Keyed(TKeyedArray {
235 known_items: if combination.keyed_array_entries.is_empty() {
236 None
237 } else {
238 Some(combination.keyed_array_entries)
239 },
240 parameters: if let Some((k, v)) = combination.keyed_array_parameters {
241 Some((Box::new(k), Box::new(v)))
242 } else {
243 None
244 },
245 non_empty: combination.flags.contains(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED),
246 }));
247 }
248
249 if let Some(list_parameter) = combination.list_array_parameter {
250 arrays.push(TArray::List(TList {
251 known_elements: if combination.list_array_entries.is_empty() {
252 None
253 } else {
254 Some(combination.list_array_entries)
255 },
256 element_type: Box::new(list_parameter),
257 non_empty: combination.flags.contains(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED),
258 known_count: None,
259 }));
260 }
261
262 for array in combination.sealed_arrays {
263 arrays.push(array);
264 }
265
266 if arrays.is_empty() && combination.flags.contains(CombinationFlags::HAS_EMPTY_ARRAY) {
267 arrays.push(TArray::Keyed(TKeyedArray { known_items: None, parameters: None, non_empty: false }));
268 }
269
270 new_types.extend(arrays.into_iter().map(TAtomic::Array));
271
272 for (_, (generic_type, generic_type_parameters)) in combination.object_type_params {
273 let generic_object = TAtomic::Object(TObject::Named(
274 TNamedObject::new(generic_type)
275 .with_is_this(*combination.object_static.get(&generic_type).unwrap_or(&false))
276 .with_type_parameters(Some(generic_type_parameters)),
277 ));
278
279 new_types.push(generic_object);
280 }
281
282 new_types.extend(combination.literal_strings.into_iter().map(|s| TAtomic::Scalar(TScalar::literal_string(s))));
283
284 if combination.value_types.contains_key(&*ATOM_STRING)
285 && combination.value_types.contains_key(&*ATOM_FLOAT)
286 && combination.value_types.contains_key(&*ATOM_BOOL)
287 && combination.integers.iter().any(super::atomic::scalar::int::TInteger::is_unspecified)
288 {
289 combination.integers.clear();
290 combination.value_types.remove(&*ATOM_STRING);
291 combination.value_types.remove(&*ATOM_FLOAT);
292 combination.value_types.remove(&*ATOM_BOOL);
293
294 new_types.push(TAtomic::Scalar(TScalar::Generic));
295 }
296
297 new_types.extend(TInteger::combine(combination.integers));
298 new_types.extend(combination.literal_floats.into_iter().map(|f| TAtomic::Scalar(TScalar::literal_float(f.into()))));
299
300 for (enum_name, enum_case) in combination.enum_names {
301 if combination.value_types.contains_key(&enum_name) {
302 continue;
303 }
304
305 let enum_object = match enum_case {
306 Some(case) => TAtomic::Object(TObject::new_enum_case(enum_name, case)),
307 None => TAtomic::Object(TObject::new_enum(enum_name)),
308 };
309
310 combination.value_types.insert(enum_object.get_id(), enum_object);
311 }
312
313 let mut has_never = combination.value_types.contains_key(&*ATOM_NEVER);
314
315 let combination_value_type_count = combination.value_types.len();
316 let mixed_from_loop_isset = combination.flags.mixed_from_loop_isset().unwrap_or(false);
317
318 for (_, atomic) in combination.value_types {
319 let tc = usize::from(has_never);
320 if atomic.is_mixed()
321 && mixed_from_loop_isset
322 && (combination_value_type_count > (tc + 1) || new_types.len() > tc)
323 {
324 continue;
325 }
326
327 if (atomic.is_never() || atomic.is_templated_as_never())
328 && (combination_value_type_count > 1 || !new_types.is_empty())
329 {
330 has_never = true;
331 continue;
332 }
333
334 new_types.push(atomic);
335 }
336
337 if new_types.is_empty() {
338 if !has_never {
339 unreachable!("No types to return, but no 'never' type found in combination.");
340 }
341
342 return vec![TAtomic::Never];
343 }
344
345 new_types
346}
347
348fn finalize_sealed_arrays(arrays: &mut Vec<TArray>, codebase: &CodebaseMetadata) {
349 if arrays.len() <= 1 {
350 return;
351 }
352
353 arrays.sort_unstable_by_key(|a| match a {
354 TArray::List(list) => list.known_elements.as_ref().map_or(0, std::collections::BTreeMap::len),
355 TArray::Keyed(keyed) => keyed.known_items.as_ref().map_or(0, std::collections::BTreeMap::len),
356 });
357
358 let mut keep = vec![true; arrays.len()];
359
360 for i in 0..arrays.len() {
361 if !keep[i] {
362 continue;
363 }
364
365 for j in (i + 1)..arrays.len() {
366 if !keep[j] {
367 continue;
368 }
369
370 if is_array_contained_by_array(codebase, &arrays[i], &arrays[j], false, &mut ComparisonResult::new()) {
371 keep[i] = false;
372 break;
373 }
374
375 if is_array_contained_by_array(codebase, &arrays[j], &arrays[i], false, &mut ComparisonResult::new()) {
376 keep[j] = false;
377 }
378 }
379 }
380
381 let mut write = 0;
382 for (read, item) in keep.iter().enumerate().take(arrays.len()) {
383 if *item {
384 if write != read {
385 arrays.swap(write, read);
386 }
387
388 write += 1;
389 }
390 }
391
392 arrays.truncate(write);
393}
394
395fn scrape_type_properties(
396 atomic: TAtomic,
397 combination: &mut TypeCombination,
398 codebase: &CodebaseMetadata,
399 options: CombinerOptions,
400) {
401 if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
402 return;
403 }
404
405 if let TAtomic::Mixed(mixed) = atomic {
406 if mixed.is_isset_from_loop() {
407 if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
408 return; }
410
411 if combination.flags.mixed_from_loop_isset().is_none() {
412 combination.flags.set_mixed_from_loop_isset(Some(true));
413 }
414
415 combination.value_types.insert(*ATOM_MIXED, atomic);
416
417 return;
418 }
419
420 combination.flags.insert(CombinationFlags::HAS_MIXED);
421
422 if mixed.is_vanilla() {
423 combination.flags.set_falsy_mixed(Some(false));
424 combination.flags.set_truthy_mixed(Some(false));
425 combination.flags.set_mixed_from_loop_isset(Some(false));
426 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
427
428 return;
429 }
430
431 if mixed.is_truthy() {
432 if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
433 return;
434 }
435
436 combination.flags.set_mixed_from_loop_isset(Some(false));
437
438 if combination.flags.falsy_mixed().unwrap_or(false) {
439 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
440 combination.flags.set_falsy_mixed(Some(false));
441 return;
442 }
443
444 if combination.flags.truthy_mixed().is_some() {
445 return;
446 }
447
448 for existing_value_type in combination.value_types.values() {
449 if !existing_value_type.is_truthy() {
450 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
451 return;
452 }
453 }
454
455 combination.flags.set_truthy_mixed(Some(true));
456 } else {
457 combination.flags.set_truthy_mixed(Some(false));
458 }
459
460 if mixed.is_falsy() {
461 if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
462 return;
463 }
464
465 combination.flags.set_mixed_from_loop_isset(Some(false));
466
467 if combination.flags.truthy_mixed().unwrap_or(false) {
468 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
469 combination.flags.set_truthy_mixed(Some(false));
470 return;
471 }
472
473 if combination.flags.falsy_mixed().is_some() {
474 return;
475 }
476
477 for existing_value_type in combination.value_types.values() {
478 if !existing_value_type.is_falsy() {
479 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
480 return;
481 }
482 }
483
484 combination.flags.set_falsy_mixed(Some(true));
485 } else {
486 combination.flags.set_falsy_mixed(Some(false));
487 }
488
489 if mixed.is_non_null() {
490 if combination.flags.contains(CombinationFlags::GENERIC_MIXED) {
491 return;
492 }
493
494 combination.flags.set_mixed_from_loop_isset(Some(false));
495
496 if combination.value_types.contains_key(&*ATOM_NULL) {
497 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
498 return;
499 }
500
501 if combination.flags.falsy_mixed().unwrap_or(false) {
502 combination.flags.set_falsy_mixed(Some(false));
503 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
504 return;
505 }
506
507 if combination.flags.nonnull_mixed().is_some() {
508 return;
509 }
510
511 combination.flags.set_mixed_from_loop_isset(Some(false));
512 combination.flags.set_nonnull_mixed(Some(true));
513 } else {
514 combination.flags.set_nonnull_mixed(Some(false));
515 }
516
517 return;
518 }
519
520 if combination.flags.falsy_mixed().unwrap_or(false) {
521 if !atomic.is_falsy() {
522 combination.flags.set_falsy_mixed(Some(false));
523 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
524 }
525
526 return;
527 }
528
529 if combination.flags.truthy_mixed().unwrap_or(false) {
530 if !atomic.is_truthy() {
531 combination.flags.set_truthy_mixed(Some(false));
532 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
533 }
534
535 return;
536 }
537
538 if combination.flags.nonnull_mixed().unwrap_or(false) {
539 if let TAtomic::Null = atomic {
540 combination.flags.set_nonnull_mixed(Some(false));
541 combination.flags.insert(CombinationFlags::GENERIC_MIXED);
542 }
543
544 return;
545 }
546
547 if combination.flags.contains(CombinationFlags::HAS_MIXED) {
548 return;
549 }
550
551 if matches!(&atomic, TAtomic::Scalar(TScalar::Bool(bool)) if !bool.is_general())
552 && combination.value_types.contains_key(&*ATOM_BOOL)
553 {
554 return;
555 }
556
557 if let TAtomic::Resource(TResource { closed }) = atomic {
558 match closed {
559 Some(closed) => {
560 if closed {
561 combination.flags.insert(CombinationFlags::CLOSED_RESOURCE);
562 } else {
563 combination.flags.insert(CombinationFlags::OPEN_RESOURCE);
564 }
565 }
566 None => {
567 combination.flags.insert(CombinationFlags::RESOURCE);
568 }
569 }
570
571 return;
572 }
573
574 if matches!(&atomic, TAtomic::Scalar(TScalar::Bool(bool)) if bool.is_general()) {
575 combination.value_types.remove(&*ATOM_FALSE);
576 combination.value_types.remove(&*ATOM_TRUE);
577 }
578
579 if let TAtomic::Array(array) = atomic {
580 if options.overwrite_empty_array && array.is_empty() {
581 combination.flags.insert(CombinationFlags::HAS_EMPTY_ARRAY);
582
583 return;
584 }
585
586 if !array.is_empty()
590 && array.is_sealed()
591 && combination.list_array_parameter.is_some()
592 && !combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY)
593 && combination.sealed_arrays.len() < options.array_combination_threshold as usize
594 {
595 combination.sealed_arrays.push(array);
596 return;
597 }
598
599 for array in std::iter::once(array).chain(combination.sealed_arrays.drain(..)) {
600 match array {
601 TArray::List(TList { element_type, known_elements, non_empty, known_count }) => {
602 if non_empty {
603 if let Some(ref mut existing_counts) = combination.list_array_counts {
604 if let Some(known_count) = known_count {
605 existing_counts.insert(known_count);
606 } else {
607 combination.list_array_counts = None;
608 }
609 }
610
611 combination.flags.insert(CombinationFlags::LIST_ARRAY_SOMETIMES_FILLED);
612 } else {
613 combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
614 }
615
616 if let Some(known_elements) = known_elements {
617 let mut has_defined_keys = false;
618
619 for (candidate_element_index, (candidate_optional, candidate_element_type)) in known_elements {
620 let existing_entry = combination.list_array_entries.get(&candidate_element_index);
621
622 let new_entry = if let Some((existing_optional, existing_type)) = existing_entry {
623 (
624 *existing_optional || candidate_optional,
625 combine_union_types(existing_type, &candidate_element_type, codebase, options),
626 )
627 } else {
628 (
629 candidate_optional,
630 if let Some(ref mut existing_value_parameter) = combination.list_array_parameter {
631 if !existing_value_parameter.is_never() {
632 *existing_value_parameter = combine_union_types(
633 existing_value_parameter,
634 &candidate_element_type,
635 codebase,
636 options,
637 );
638
639 continue;
640 }
641
642 candidate_element_type
643 } else {
644 candidate_element_type
645 },
646 )
647 };
648
649 combination.list_array_entries.insert(candidate_element_index, new_entry);
650
651 if !candidate_optional {
652 has_defined_keys = true;
653 }
654 }
655
656 if !has_defined_keys {
657 combination.flags.remove(CombinationFlags::LIST_ARRAY_ALWAYS_FILLED);
658 }
659 } else if !options.overwrite_empty_array {
660 if element_type.is_never() {
661 for (pu, _) in combination.list_array_entries.values_mut() {
662 *pu = true;
663 }
664 } else {
665 for (_, entry_type) in combination.list_array_entries.values() {
666 if let Some(ref mut existing_value_param) = combination.list_array_parameter {
667 *existing_value_param =
668 combine_union_types(existing_value_param, entry_type, codebase, options);
669 }
670 }
671
672 combination.list_array_entries.clear();
673 }
674 }
675
676 combination.list_array_parameter = if let Some(ref existing_type) = combination.list_array_parameter
677 {
678 Some(combine_union_types(existing_type, &element_type, codebase, options))
679 } else {
680 Some((*element_type).clone())
681 };
682 }
683 TArray::Keyed(TKeyedArray { parameters, known_items, non_empty, .. }) => {
684 let had_previous_keyed_array = combination.flags.contains(CombinationFlags::HAS_KEYED_ARRAY);
685 combination.flags.insert(CombinationFlags::HAS_KEYED_ARRAY);
686
687 if non_empty {
688 combination.flags.insert(CombinationFlags::KEYED_ARRAY_SOMETIMES_FILLED);
689 } else {
690 combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
691 }
692
693 if let Some(known_items) = known_items {
694 let has_existing_entries =
695 !combination.keyed_array_entries.is_empty() || had_previous_keyed_array;
696 let mut possibly_undefined_entries =
697 combination.keyed_array_entries.keys().copied().collect::<HashSet<_>>();
698
699 let mut has_defined_keys = false;
700
701 for (candidate_item_name, (cu, candidate_item_type)) in known_items {
702 if let Some((eu, existing_type)) =
703 combination.keyed_array_entries.get_mut(&candidate_item_name)
704 {
705 if cu {
706 *eu = true;
707 }
708 if &candidate_item_type != existing_type {
709 *existing_type =
710 combine_union_types(existing_type, &candidate_item_type, codebase, options);
711 }
712 } else {
713 let new_item_value_type =
714 if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
715 combination.keyed_array_parameters
716 {
717 adjust_keyed_array_parameters(
718 existing_value_param,
719 &candidate_item_type,
720 codebase,
721 options,
722 &candidate_item_name,
723 existing_key_param,
724 );
725
726 continue;
727 } else {
728 let new_type = candidate_item_type.clone();
729 (has_existing_entries || cu, new_type)
730 };
731
732 combination.keyed_array_entries.insert(candidate_item_name, new_item_value_type);
733 }
734
735 possibly_undefined_entries.remove(&candidate_item_name);
736
737 if !cu {
738 has_defined_keys = true;
739 }
740 }
741
742 if !has_defined_keys {
743 combination.flags.remove(CombinationFlags::KEYED_ARRAY_ALWAYS_FILLED);
744 }
745
746 for possibly_undefined_type_key in possibly_undefined_entries {
747 let possibly_undefined_type =
748 combination.keyed_array_entries.get_mut(&possibly_undefined_type_key);
749 if let Some((pu, _)) = possibly_undefined_type {
750 *pu = true;
751 }
752 }
753 } else if !options.overwrite_empty_array {
754 if match ¶meters {
755 Some((_, value_param)) => value_param.is_never(),
756 None => true,
757 } {
758 for (tu, _) in combination.keyed_array_entries.values_mut() {
759 *tu = true;
760 }
761 } else {
762 for (key, (_, entry_type)) in &combination.keyed_array_entries {
763 if let Some((ref mut existing_key_param, ref mut existing_value_param)) =
764 combination.keyed_array_parameters
765 {
766 adjust_keyed_array_parameters(
767 existing_value_param,
768 entry_type,
769 codebase,
770 options,
771 key,
772 existing_key_param,
773 );
774 }
775 }
776
777 combination.keyed_array_entries.clear();
778 }
779 }
780
781 combination.keyed_array_parameters = match (&combination.keyed_array_parameters, parameters) {
782 (None, None) => None,
783 (Some(existing_types), None) => Some(existing_types.clone()),
784 (None, Some(params)) => Some(((*params.0).clone(), (*params.1).clone())),
785 (Some(existing_types), Some(params)) => Some((
786 combine_union_types(&existing_types.0, ¶ms.0, codebase, options),
787 combine_union_types(&existing_types.1, ¶ms.1, codebase, options),
788 )),
789 };
790 }
791 }
792 }
793
794 return;
795 }
796
797 if let TAtomic::Object(TObject::Any) = atomic {
800 combination.flags.insert(CombinationFlags::HAS_OBJECT_TOP_TYPE);
801 combination.value_types.retain(|_, t| !matches!(t, TAtomic::Object(TObject::Named(_))));
802 combination.value_types.insert(atomic.get_id(), atomic);
803
804 return;
805 }
806
807 if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
808 if let Some(object_static) = combination.object_static.get(named_object.get_name_ref()) {
809 if *object_static && !named_object.is_this() {
810 combination.object_static.insert(named_object.get_name(), false);
811 }
812 } else {
813 combination.object_static.insert(named_object.get_name(), named_object.is_this());
814 }
815 }
816
817 if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
818 let fq_class_name = named_object.get_name();
819 if let Some(type_parameters) = named_object.get_type_parameters() {
820 let object_type_key = get_combiner_key(fq_class_name, type_parameters, codebase);
821
822 if let Some((_, existing_type_params)) = combination.object_type_params.get(&object_type_key) {
823 let mut new_type_parameters = Vec::with_capacity(type_parameters.len());
824 for (i, type_param) in type_parameters.iter().enumerate() {
825 if let Some(existing_type_param) = existing_type_params.get(i) {
826 new_type_parameters.push(combine_union_types(
827 existing_type_param,
828 type_param,
829 codebase,
830 options,
831 ));
832 }
833 }
834
835 combination.object_type_params.insert(object_type_key, (fq_class_name, new_type_parameters));
836 } else {
837 combination.object_type_params.insert(object_type_key, (fq_class_name, type_parameters.to_vec()));
838 }
839
840 return;
841 }
842 }
843
844 if let TAtomic::Object(TObject::Enum(enum_object)) = atomic {
845 combination.enum_names.insert((enum_object.get_name(), enum_object.get_case()));
846
847 return;
848 }
849
850 if let TAtomic::Object(TObject::Named(named_object)) = &atomic {
851 let fq_class_name = named_object.get_name();
852 let intersection_types = named_object.get_intersection_types();
853
854 if combination.flags.contains(CombinationFlags::HAS_OBJECT_TOP_TYPE)
855 || combination.value_types.contains_key(&atomic.get_id())
856 {
857 return;
858 }
859
860 let Some(symbol_type) = codebase.symbols.get_kind(&fq_class_name) else {
861 combination.value_types.insert(atomic.get_id(), atomic);
862 return;
863 };
864
865 if !matches!(symbol_type, SymbolKind::Class | SymbolKind::Enum | SymbolKind::Interface) {
866 combination.value_types.insert(atomic.get_id(), atomic);
867 return;
868 }
869
870 let is_class = matches!(symbol_type, SymbolKind::Class);
871 let is_interface = matches!(symbol_type, SymbolKind::Interface);
872
873 let mut types_to_remove: Vec<Atom> = Vec::new();
874
875 for (key, existing_type) in &combination.value_types {
876 if let TAtomic::Object(TObject::Named(existing_object)) = &existing_type {
877 let existing_name = existing_object.get_name();
878
879 if intersection_types.is_some() || existing_object.has_intersection_types() {
880 if object_comparator::is_shallowly_contained_by(
881 codebase,
882 existing_type,
883 &atomic,
884 false,
885 &mut ComparisonResult::new(),
886 ) {
887 types_to_remove.push(existing_name);
888 continue;
889 }
890
891 if object_comparator::is_shallowly_contained_by(
892 codebase,
893 &atomic,
894 existing_type,
895 false,
896 &mut ComparisonResult::new(),
897 ) {
898 return;
899 }
900
901 continue;
902 }
903
904 let Some(existing_symbol_kind) = codebase.symbols.get_kind(existing_object.get_name_ref()) else {
905 continue;
906 };
907
908 if matches!(existing_symbol_kind, SymbolKind::Class) {
909 if codebase.is_instance_of(&existing_name, &fq_class_name) {
911 types_to_remove.push(*key);
912 continue;
913 }
914
915 if is_class {
916 if codebase.class_extends(&fq_class_name, &existing_name) {
918 return;
919 }
920 } else if is_interface {
921 if codebase.class_implements(&fq_class_name, &existing_name) {
923 return;
924 }
925 }
926 } else if matches!(existing_symbol_kind, SymbolKind::Interface) {
927 if codebase.class_implements(&existing_name, &fq_class_name) {
928 types_to_remove.push(existing_name);
929 continue;
930 }
931
932 if (is_class || is_interface) && codebase.class_implements(&fq_class_name, &existing_name) {
933 return;
934 }
935 }
936 }
937 }
938
939 combination.value_types.insert(atomic.get_id(), atomic);
940
941 for type_key in types_to_remove {
942 combination.value_types.remove(&type_key);
943 }
944
945 return;
946 }
947
948 if let TAtomic::Scalar(TScalar::Generic) = atomic {
949 combination.literal_strings.clear();
950 combination.integers.clear();
951 combination.literal_floats.clear();
952 combination.value_types.retain(|k, _| {
953 k != "string"
954 && k != "bool"
955 && k != "false"
956 && k != "true"
957 && k != "float"
958 && k != "numeric"
959 && k != "array-key"
960 });
961
962 combination.value_types.insert(atomic.get_id(), atomic);
963 return;
964 }
965
966 if let TAtomic::Scalar(TScalar::ArrayKey) = atomic {
967 if combination.value_types.contains_key(&*ATOM_SCALAR) {
968 return;
969 }
970
971 combination.literal_strings.clear();
972 combination.integers.clear();
973 combination.value_types.retain(|k, _| k != &*ATOM_STRING && k != &*ATOM_INT);
974 combination.value_types.insert(atomic.get_id(), atomic);
975
976 return;
977 }
978
979 if let TAtomic::Scalar(TScalar::String(_) | TScalar::Integer(_)) = atomic
980 && (combination.value_types.contains_key(&*ATOM_SCALAR)
981 || combination.value_types.contains_key(&*ATOM_ARRAY_KEY))
982 {
983 return;
984 }
985
986 if let TAtomic::Scalar(TScalar::Float(_) | TScalar::Integer(_)) = atomic
987 && (combination.value_types.contains_key(&*ATOM_NUMERIC) || combination.value_types.contains_key(&*ATOM_SCALAR))
988 {
989 return;
990 }
991
992 if let TAtomic::Scalar(TScalar::String(mut string_scalar)) = atomic {
993 if let Some(existing_string_type) = combination.value_types.get_mut(&*ATOM_STRING) {
994 if let TAtomic::Scalar(TScalar::String(existing_string_type)) = existing_string_type {
995 if let Some(lit_atom) = string_scalar.get_known_literal_atom() {
996 let lit_value = lit_atom.as_str();
997 let is_incompatible = (existing_string_type.is_numeric && !str_is_numeric(lit_value))
998 || (existing_string_type.is_truthy && (lit_value.is_empty() || lit_value == "0"))
999 || (existing_string_type.is_non_empty && lit_value.is_empty())
1000 || (existing_string_type.is_lowercase && lit_value.chars().any(char::is_uppercase));
1001
1002 if is_incompatible {
1003 if combination.literal_strings.len() >= options.string_combination_threshold as usize {
1005 *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1007 } else {
1008 combination.literal_strings.insert(lit_atom);
1009 }
1010 } else {
1011 *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1012 }
1013 } else {
1014 *existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
1015 }
1016 }
1017 } else if let Some(atom) = string_scalar.get_known_literal_atom() {
1018 if combination.literal_strings.len() >= options.string_combination_threshold as usize {
1020 combination.literal_strings.clear();
1022 combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::string()));
1023 } else {
1024 combination.literal_strings.insert(atom);
1025 }
1026 } else {
1027 let mut literals_to_keep = Vec::new();
1031
1032 if string_scalar.is_truthy
1033 || string_scalar.is_non_empty
1034 || string_scalar.is_numeric
1035 || string_scalar.is_lowercase
1036 {
1037 for value in &combination.literal_strings {
1038 if value.is_empty() {
1039 string_scalar.is_non_empty = false;
1040 string_scalar.is_truthy = false;
1041 string_scalar.is_numeric = false;
1042 break;
1043 } else if value == "0" {
1044 string_scalar.is_truthy = false;
1045 }
1046
1047 if string_scalar.is_numeric && !str_is_numeric(value) {
1049 literals_to_keep.push(*value);
1050 } else {
1051 string_scalar.is_numeric = string_scalar.is_numeric && str_is_numeric(value);
1052 }
1053
1054 string_scalar.is_lowercase = string_scalar.is_lowercase && value.chars().all(|c| !c.is_uppercase());
1055 }
1056 }
1057
1058 combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::String(string_scalar)));
1059
1060 combination.literal_strings.clear();
1061 for lit in literals_to_keep {
1062 combination.literal_strings.insert(lit);
1063 }
1064 }
1065
1066 return;
1067 }
1068
1069 if let TAtomic::Scalar(TScalar::Integer(integer)) = &atomic {
1070 if combination.value_types.contains_key(&*ATOM_INT) {
1072 return;
1073 }
1074
1075 if integer.is_literal() && combination.integers.len() >= options.integer_combination_threshold as usize {
1077 combination.integers.clear();
1079 combination.value_types.insert(*ATOM_INT, TAtomic::Scalar(TScalar::int()));
1080 return;
1081 }
1082
1083 combination.integers.push(*integer);
1084
1085 return;
1086 }
1087
1088 if let TAtomic::Scalar(TScalar::Float(float_scalar)) = &atomic {
1089 if combination.value_types.contains_key(&*ATOM_FLOAT) {
1090 return;
1091 }
1092
1093 if let TFloat::Literal(literal_value) = float_scalar {
1094 if combination.literal_floats.len() >= options.string_combination_threshold as usize {
1096 combination.literal_floats.clear();
1098 combination.value_types.insert(*ATOM_FLOAT, TAtomic::Scalar(TScalar::float()));
1099 return;
1100 }
1101 combination.literal_floats.push(*literal_value);
1102 } else {
1103 combination.literal_floats.clear();
1104 combination.value_types.insert(*ATOM_FLOAT, atomic);
1105 }
1106
1107 return;
1108 }
1109
1110 combination.value_types.insert(atomic.get_id(), atomic);
1111}
1112
1113fn adjust_keyed_array_parameters(
1114 existing_value_param: &mut TUnion,
1115 entry_type: &TUnion,
1116 codebase: &CodebaseMetadata,
1117 options: CombinerOptions,
1118 key: &ArrayKey,
1119 existing_key_param: &mut TUnion,
1120) {
1121 *existing_value_param = combine_union_types(existing_value_param, entry_type, codebase, options);
1122 let new_key_type = key.to_union();
1123 *existing_key_param = combine_union_types(existing_key_param, &new_key_type, codebase, options);
1124}
1125
1126const COMBINER_KEY_STACK_BUF: usize = 256;
1127
1128fn get_combiner_key(name: Atom, type_params: &[TUnion], codebase: &CodebaseMetadata) -> Atom {
1129 let covariants = if let Some(class_like_metadata) = codebase.get_class_like(&name) {
1130 &class_like_metadata.template_variance
1131 } else {
1132 return name;
1133 };
1134
1135 let name_str = name.as_str();
1136 let mut estimated_len = name_str.len() + 2; for (i, tunion) in type_params.iter().enumerate() {
1138 if i > 0 {
1139 estimated_len += 2; }
1141
1142 if covariants.get(&i) == Some(&Variance::Covariant) {
1143 estimated_len += 1; } else {
1145 estimated_len += tunion.get_id().len();
1146 }
1147 }
1148
1149 if estimated_len <= COMBINER_KEY_STACK_BUF {
1150 let mut buffer = [0u8; COMBINER_KEY_STACK_BUF];
1151 let mut pos = 0;
1152
1153 buffer[pos..pos + name_str.len()].copy_from_slice(name_str.as_bytes());
1154 pos += name_str.len();
1155
1156 buffer[pos] = b'<';
1157 pos += 1;
1158
1159 for (i, tunion) in type_params.iter().enumerate() {
1160 if i > 0 {
1161 buffer[pos..pos + 2].copy_from_slice(b", ");
1162 pos += 2;
1163 }
1164 let param_str =
1165 if covariants.get(&i) == Some(&Variance::Covariant) { "*" } else { tunion.get_id().as_str() };
1166 buffer[pos..pos + param_str.len()].copy_from_slice(param_str.as_bytes());
1167 pos += param_str.len();
1168 }
1169
1170 buffer[pos] = b'>';
1171 pos += 1;
1172
1173 return atom(unsafe { std::str::from_utf8_unchecked(&buffer[..pos]) });
1175 }
1176
1177 let mut result = String::with_capacity(estimated_len);
1178 result.push_str(name_str);
1179 result.push('<');
1180 for (i, tunion) in type_params.iter().enumerate() {
1181 if i > 0 {
1182 result.push_str(", ");
1183 }
1184 if covariants.get(&i) == Some(&Variance::Covariant) {
1185 result.push('*');
1186 } else {
1187 result.push_str(tunion.get_id().as_str());
1188 }
1189 }
1190 result.push('>');
1191 atom(&result)
1192}
1193
1194fn combine_string_scalars(s1: &TString, s2: TString) -> TString {
1195 TString {
1196 literal: match (&s1.literal, s2.literal) {
1197 (Some(TStringLiteral::Value(v1)), Some(TStringLiteral::Value(v2))) => {
1198 if v1 == &v2 {
1199 Some(TStringLiteral::Value(v2))
1200 } else {
1201 Some(TStringLiteral::Unspecified)
1202 }
1203 }
1204 (Some(TStringLiteral::Unspecified), Some(_)) | (Some(_), Some(TStringLiteral::Unspecified)) => {
1205 Some(TStringLiteral::Unspecified)
1206 }
1207 _ => None,
1208 },
1209 is_numeric: s1.is_numeric && s2.is_numeric,
1210 is_truthy: s1.is_truthy && s2.is_truthy,
1211 is_non_empty: s1.is_non_empty && s2.is_non_empty,
1212 is_lowercase: s1.is_lowercase && s2.is_lowercase,
1213 }
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218 use std::collections::BTreeMap;
1219
1220 use super::*;
1221
1222 use crate::ttype::atomic::TAtomic;
1223 use crate::ttype::atomic::array::list::TList;
1224 use crate::ttype::atomic::scalar::TScalar;
1225
1226 #[test]
1227 fn test_combine_scalars() {
1228 let types = vec![
1229 TAtomic::Scalar(TScalar::string()),
1230 TAtomic::Scalar(TScalar::int()),
1231 TAtomic::Scalar(TScalar::float()),
1232 TAtomic::Scalar(TScalar::bool()),
1233 ];
1234
1235 let combined =
1236 combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1237
1238 assert_eq!(combined.len(), 1);
1239 assert!(matches!(combined[0], TAtomic::Scalar(TScalar::Generic)));
1240 }
1241
1242 #[test]
1243 fn test_combine_boolean_lists() {
1244 let types = vec![
1245 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1246 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#false())))),
1247 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#true())))),
1248 ])))),
1249 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1250 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#true())))),
1251 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::r#false())))),
1252 ])))),
1253 ];
1254
1255 let combined =
1256 combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1257
1258 assert_eq!(combined.len(), 2);
1259 assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1260 assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1261 }
1262
1263 #[test]
1264 fn test_combine_integer_lists() {
1265 let types = vec![
1266 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1267 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1268 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1269 ])))),
1270 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1271 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1272 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1273 ])))),
1274 ];
1275
1276 let combined =
1277 combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1278
1279 assert_eq!(combined.len(), 2);
1280 assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1281 assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1282 }
1283
1284 #[test]
1285 fn test_combine_string_lists() {
1286 let types = vec![
1287 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1288 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1289 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1290 ])))),
1291 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1292 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1293 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1294 ])))),
1295 ];
1296
1297 let combined =
1298 combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1299
1300 assert_eq!(combined.len(), 2);
1301 assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1302 assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1303 }
1304
1305 #[test]
1306 fn test_combine_mixed_literal_lists() {
1307 let types = vec![
1308 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1309 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1310 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("a".into())))))),
1311 ])))),
1312 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1313 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal("b".into())))))),
1314 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1315 ])))),
1316 ];
1317
1318 let combined =
1319 combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1320
1321 assert_eq!(combined.len(), 2);
1322 assert!(matches!(combined[0], TAtomic::Array(TArray::List(_))));
1323 assert!(matches!(combined[1], TAtomic::Array(TArray::List(_))));
1324 }
1325
1326 #[test]
1327 fn test_combine_list_with_generic_list() {
1328 let types = vec![
1329 TAtomic::Array(TArray::List(TList::from_known_elements(BTreeMap::from_iter([
1330 (0, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(1)))))),
1331 (1, (false, TUnion::from_atomic(TAtomic::Scalar(TScalar::Integer(TInteger::literal(2)))))),
1332 ])))),
1333 TAtomic::Array(TArray::List(TList::new(Box::new(TUnion::from_atomic(TAtomic::Scalar(TScalar::int())))))), ];
1335
1336 let combined =
1337 combine(types, &CodebaseMetadata::default(), CombinerOptions::default().with_overwrite_empty_array());
1338
1339 assert_eq!(combined.len(), 1);
1341
1342 let TAtomic::Array(TArray::List(list_type)) = &combined[0] else {
1343 panic!("Expected a list type");
1344 };
1345
1346 let Some(known_elements) = &list_type.known_elements else {
1347 panic!("Expected known elements");
1348 };
1349
1350 assert!(!list_type.is_non_empty());
1351 assert!(list_type.known_count.is_none());
1352 assert!(list_type.element_type.is_int());
1353
1354 assert_eq!(known_elements.len(), 2);
1355 assert!(known_elements.contains_key(&0));
1356 assert!(known_elements.contains_key(&1));
1357
1358 let Some(first_element) = known_elements.get(&0) else {
1359 panic!("Expected first element");
1360 };
1361
1362 let Some(second_element) = known_elements.get(&1) else {
1363 panic!("Expected second element");
1364 };
1365
1366 assert!(first_element.1.is_int());
1367 assert!(second_element.1.is_int());
1368 }
1369}