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