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