1use std::{cmp::Reverse, collections::hash_map::Entry, hash::Hash};
2
3use anyhow::{Context, Result, bail};
4use rustc_hash::{FxHashMap, FxHashSet};
5use tracing::Instrument;
6use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
7
8use crate::{
9 chunk::{
10 ChunkableModule, ChunkingType, MergeableModule, MergeableModuleExposure, MergeableModules,
11 MergeableModulesExposed,
12 },
13 module::Module,
14 module_graph::{
15 GraphTraversalAction, ModuleGraph, RefData, chunk_group_info::RoaringBitmapWrapper,
16 },
17 resolve::ExportUsage,
18};
19
20#[turbo_tasks::value(transparent, cell = "keyed")]
21#[allow(clippy::type_complexity)]
22pub struct MergedModulesReplacements(
23 FxHashMap<ResolvedVc<Box<dyn Module>>, Option<ResolvedVc<Box<dyn ChunkableModule>>>>,
24);
25
26#[turbo_tasks::value(transparent, cell = "keyed")]
27#[allow(clippy::type_complexity)]
28pub struct MergedModulesOriginalModules(
29 FxHashMap<ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn Module>>>,
30);
31
32#[turbo_tasks::value]
33pub struct MergedModuleInfo {
34 pub replacements: ResolvedVc<MergedModulesReplacements>,
42 pub replacements_to_original: ResolvedVc<MergedModulesOriginalModules>,
45}
46
47impl MergedModuleInfo {
48 pub async fn should_replace_module(
53 &self,
54 module: ResolvedVc<Box<dyn Module>>,
55 ) -> Result<Option<Option<ResolvedVc<Box<dyn ChunkableModule>>>>> {
56 Ok(self.replacements.get(&module).await?.as_deref().copied())
57 }
58
59 pub async fn get_original_module(
62 &self,
63 module: ResolvedVc<Box<dyn Module>>,
64 ) -> Result<Option<ResolvedVc<Box<dyn Module>>>> {
65 Ok(self
66 .replacements_to_original
67 .get(&module)
68 .await?
69 .as_deref()
70 .copied())
71 }
72}
73
74fn intern_bitmaps<'a, K: Eq + Hash>(
78 entries: impl IntoIterator<Item = (K, &'a RoaringBitmapWrapper)>,
79 capacity: usize,
80) -> (FxHashMap<K, u32>, usize) {
81 let mut ids: FxHashMap<&'a RoaringBitmapWrapper, u32> = FxHashMap::default();
82 let mut result = FxHashMap::with_capacity_and_hasher(capacity, Default::default());
83 for (key, bitmap) in entries {
84 let next_id = ids.len() as u32;
85 result.insert(key, *ids.entry(bitmap).or_insert(next_id));
86 }
87 (result, ids.len())
88}
89
90pub async fn compute_merged_modules(module_graph: Vc<ModuleGraph>) -> Result<Vc<MergedModuleInfo>> {
96 let span_outer = tracing::info_span!(
97 "compute merged modules",
98 module_count = tracing::field::Empty,
99 visit_count = tracing::field::Empty,
100 merged_groups = tracing::field::Empty,
101 included_modules = tracing::field::Empty
102 );
103
104 let span = span_outer.clone();
105 async move {
106 let async_module_info = module_graph.async_module_info();
107 let chunk_group_info = module_graph.chunk_group_info().await?;
108 let module_graph = module_graph.await?;
109
110 let graphs = &module_graph.graphs;
111 let module_count = graphs.iter().map(|g| g.graph.node_count()).sum::<usize>();
112 span.record("module_count", module_count);
113
114 let entries = module_graph
116 .all_chunk_group_entry_modules()
117 .collect::<Vec<_>>();
118
119 let module_rank = {
122 let _inner_span = tracing::info_span!("compute reverse postorder").entered();
123 module_graph.reverse_postorder_ranks(entries.iter().copied())?
124 };
125
126 let mut module_merged_groups: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
130 FxHashMap::with_capacity_and_hasher(module_count, Default::default());
131 let mut entry_modules =
133 FxHashSet::with_capacity_and_hasher(module_count, Default::default());
134
135 let inner_span = tracing::info_span!("collect mergeable modules");
136 let mergeable = module_graph
137 .iter_reachable_modules()?
138 .map(async |module| {
139 if let Some(mergeable) =
140 ResolvedVc::try_downcast::<Box<dyn MergeableModule>>(module)
141 && *mergeable.is_mergeable().await?
142 {
143 return Ok(Some(module));
144 }
145 Ok(None)
146 })
147 .try_flat_join()
148 .instrument(inner_span)
149 .await?
150 .into_iter()
151 .collect::<FxHashSet<_>>();
152
153 let inner_span = tracing::info_span!("pre-fetch async module status");
156 let async_modules: FxHashSet<_> = mergeable
157 .iter()
158 .map(async |&module| Ok(async_module_info.is_async(module).await?.then_some(module)))
159 .try_flat_join()
160 .instrument(inner_span)
161 .await?
162 .into_iter()
163 .collect();
164
165 let inner_span = tracing::info_span!("fixed point traversal").entered();
166
167 let mut next_index = 0u32;
168 let visit_count = module_graph.traverse_edges_fixed_point_with_priority(
169 entries
170 .iter()
171 .map(|e| {
172 Ok((
173 *e,
174 Reverse(*module_rank.get(e).context("Module rank not found")?),
175 ))
176 })
177 .collect::<Result<Vec<_>>>()?,
178 &mut (),
179 |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
180 node: ResolvedVc<Box<dyn Module>>,
181 _,
182 _|
183 -> Result<GraphTraversalAction> {
184 let (parent_module, hoisted) =
187 parent_info.map_or((None, false), |(node, ty, _)| {
188 (
189 Some(node),
190 match &ty.chunking_type {
191 ChunkingType::Parallel { hoisted, .. } => *hoisted,
192 _ => false,
193 },
194 )
195 });
196 let module = node;
197
198 Ok(if parent_module.is_some_and(|p| p == module) {
199 GraphTraversalAction::Skip
201 } else if hoisted
202 && let Some(parent_module) = parent_module
203 && mergeable.contains(&parent_module)
204 && mergeable.contains(&module)
205 && !async_modules.contains(&parent_module)
206 && !async_modules.contains(&module)
207 {
208 module_merged_groups.entry(node).or_default();
213 let [Some(parent_merged_groups), Some(current_merged_groups)] =
214 module_merged_groups.get_disjoint_mut([&parent_module, &node])
215 else {
216 bail!("unreachable except for eventual consistency");
218 };
219
220 if current_merged_groups.is_empty() {
221 *current_merged_groups = parent_merged_groups.clone();
223 GraphTraversalAction::Continue
224 } else if parent_merged_groups.is_proper_superset(current_merged_groups) {
225 **current_merged_groups |= &**parent_merged_groups;
227 GraphTraversalAction::Continue
228 } else {
229 GraphTraversalAction::Skip
231 }
232 } else {
233 if entry_modules.insert(module) {
236 let idx = next_index;
238 next_index += 1;
239
240 if module_merged_groups.entry(module).or_default().insert(idx) {
241 GraphTraversalAction::Continue
243 } else {
244 GraphTraversalAction::Skip
246 }
247 } else {
248 GraphTraversalAction::Skip
251 }
252 })
253 },
254 |successor, _| {
255 Ok(Reverse(
258 *module_rank
259 .get(&successor)
260 .context("Module rank not found")?,
261 ))
262 },
263 )?;
264
265 drop(inner_span);
266
267 let module_bitmap_ids = {
270 let _inner_span = tracing::info_span!("intern bitmaps").entered();
271 intern_bitmaps(
272 module_merged_groups
273 .iter()
274 .map(|(&module, bitmap)| (module, bitmap)),
275 module_merged_groups.len(),
276 )
277 .0
278 };
279 let bitmap_id = |module: ResolvedVc<Box<dyn Module>>| {
280 module_bitmap_ids
281 .get(&module)
282 .copied()
283 .context("every module should have a bitmap")
284 };
285
286 let inner_span = tracing::info_span!("chunk group collection").entered();
287
288 span.record("visit_count", visit_count);
289
290 #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
291 struct ListOccurrence {
292 entry: usize,
295 list: usize,
296 chunk_group: usize,
297 }
298
299 let mut lists;
304 let mut lists_reverse_indices: FxIndexMap<
305 ResolvedVc<Box<dyn MergeableModule>>,
306 FxIndexSet<ListOccurrence>,
307 > = FxIndexMap::default();
308
309 #[allow(non_snake_case)]
312 let LISTS_COMMON_IDX: usize;
313
314 #[allow(clippy::type_complexity)]
318 let mut intra_group_references: FxIndexMap<
319 ResolvedVc<Box<dyn Module>>,
320 FxIndexSet<ResolvedVc<Box<dyn Module>>>,
321 > = FxIndexMap::default();
322 #[allow(clippy::type_complexity)]
325 let mut intra_group_references_rev: FxIndexMap<
326 ResolvedVc<Box<dyn Module>>,
327 FxIndexSet<ResolvedVc<Box<dyn Module>>>,
328 > = FxIndexMap::default();
329
330 {
331 struct ChunkGroupResult {
332 first_chunk_group_idx: usize,
333 #[allow(clippy::type_complexity)]
334 list_lists: Vec<Vec<Vec<ResolvedVc<Box<dyn MergeableModule>>>>>,
335 lists_reverse_indices:
336 FxIndexMap<ResolvedVc<Box<dyn MergeableModule>>, FxIndexSet<ListOccurrence>>,
337 #[allow(clippy::type_complexity)]
338 intra_group_references_rev: FxIndexMap<
339 ResolvedVc<Box<dyn Module>>,
340 FxIndexSet<ResolvedVc<Box<dyn Module>>>,
341 >,
342 }
343 let span = tracing::info_span!("map chunk groups").entered();
344
345 let result = turbo_tasks::parallel::map_collect_chunked_owned::<_, _, Result<Vec<_>>>(
346 chunk_group_info.chunk_groups.iter().enumerate().collect(),
348 |chunk| {
349 let mut list_lists = vec![];
350 let mut lists_reverse_indices: FxIndexMap<
351 ResolvedVc<Box<dyn MergeableModule>>,
352 FxIndexSet<ListOccurrence>,
353 > = FxIndexMap::default();
354 #[allow(clippy::type_complexity)]
355 let mut intra_group_references_rev: FxIndexMap<
356 ResolvedVc<Box<dyn Module>>,
357 FxIndexSet<ResolvedVc<Box<dyn Module>>>,
358 > = FxIndexMap::default();
359
360 let mut chunk = chunk.peekable();
361 let first_chunk_group_idx = chunk.peek().unwrap().0;
362
363 for (chunk_group_idx, chunk_group) in chunk {
364 let mut lists = vec![];
365
366 let mut chunk_lists: FxHashMap<u32, usize> =
371 FxHashMap::with_capacity_and_hasher(
372 module_merged_groups.len() / chunk_group_info.chunk_groups.len(),
373 Default::default(),
374 );
375
376 let mut visited = FxHashSet::default();
380
381 module_graph.traverse_edges_dfs(
382 chunk_group.entries(),
383 &mut (),
384 |parent_info, node, _| {
385 if parent_info.is_none_or(|(_, r)| r.chunking_type.is_parallel())
386 && visited.insert(node)
387 {
388 Ok(GraphTraversalAction::Continue)
389 } else {
390 Ok(GraphTraversalAction::Exclude)
391 }
392 },
393 |parent_info, node, _| {
394 let module = node;
395 let bitmap = bitmap_id(module)?;
396
397 if mergeable.contains(&module) {
398 let mergeable_module =
399 ResolvedVc::try_downcast::<Box<dyn MergeableModule>>(
400 module,
401 )
402 .context(
403 "found mergeable module which is not a MergeableModule",
404 )?;
405 match chunk_lists.entry(bitmap) {
406 Entry::Vacant(e) => {
407 let idx = lists.len();
409 e.insert(idx);
410 lists.push(vec![mergeable_module]);
411 lists_reverse_indices
412 .entry(mergeable_module)
413 .or_default()
414 .insert(ListOccurrence {
415 chunk_group: chunk_group_idx,
416 list: idx,
417 entry: 0,
418 });
419 }
420 Entry::Occupied(e) => {
421 let list_idx = *e.get();
422 let list = &mut lists[list_idx];
423 list.push(mergeable_module);
424 lists_reverse_indices
425 .entry(mergeable_module)
426 .or_default()
427 .insert(ListOccurrence {
428 chunk_group: chunk_group_idx,
429 list: list_idx,
430 entry: list.len() - 1,
431 });
432 }
433 }
434 }
435
436 if let Some((parent, _)) = parent_info {
437 let same_bitmap = bitmap_id(parent)? == bitmap;
438
439 if same_bitmap {
440 intra_group_references_rev
441 .entry(module)
442 .or_default()
443 .insert(parent);
444 }
445 }
446 Ok(())
447 },
448 false,
449 false,
450 )?;
451
452 list_lists.push(lists);
453 }
454 Ok(ChunkGroupResult {
455 first_chunk_group_idx,
456 list_lists,
457 lists_reverse_indices,
458 intra_group_references_rev,
459 })
460 },
461 )?;
462
463 drop(span);
464 let _span = tracing::info_span!("merging chunk group lists").entered();
465
466 lists_reverse_indices
467 .reserve_exact(result.iter().map(|r| r.lists_reverse_indices.len()).sum());
468 intra_group_references_rev.reserve_exact(
469 result
470 .iter()
471 .map(|r| r.intra_group_references_rev.len())
472 .sum(),
473 );
474
475 lists = vec![Default::default(); chunk_group_info.chunk_groups.len() + 1];
476 LISTS_COMMON_IDX = result.len();
477 for ChunkGroupResult {
478 first_chunk_group_idx,
479 list_lists: result_lists,
480 lists_reverse_indices: result_lists_reverse_indices,
481 intra_group_references_rev: result_intra_group_references_rev,
482 } in result
483 {
484 lists.splice(
485 first_chunk_group_idx..(first_chunk_group_idx + result_lists.len()),
486 result_lists,
487 );
488 for (module, occurrences) in result_lists_reverse_indices {
489 lists_reverse_indices
490 .entry(module)
491 .or_default()
492 .extend(occurrences);
493 }
494 for (module, occurrences) in result_intra_group_references_rev {
495 intra_group_references_rev
496 .entry(module)
497 .or_default()
498 .extend(occurrences);
499 }
500 }
501 }
502
503 drop(inner_span);
504 let inner_span = tracing::info_span!("exposed computation").entered();
505
506 lists_reverse_indices
508 .sort_by_cached_key(|_, b| b.iter().map(|o| o.entry).min().map(|v| -(v as i64)));
509
510 let mut exposed_modules_imported: FxHashSet<ResolvedVc<Box<dyn Module>>> =
514 FxHashSet::with_capacity_and_hasher(module_merged_groups.len(), Default::default());
515 let mut exposed_modules_namespace: FxHashSet<ResolvedVc<Box<dyn Module>>> =
516 FxHashSet::with_capacity_and_hasher(module_merged_groups.len(), Default::default());
517
518 module_graph.traverse_edges_dfs(
519 entries,
520 &mut (),
521 |_, _, _| Ok(GraphTraversalAction::Continue),
522 |parent_info, node, _| {
523 let module = node;
524
525 if let Some((parent, _)) = parent_info {
526 let same_bitmap = bitmap_id(parent)? == bitmap_id(module)?;
527
528 if same_bitmap {
529 intra_group_references
530 .entry(parent)
531 .or_default()
532 .insert(module);
533 }
534 }
535
536 if match parent_info {
537 None => true,
538 Some((parent, _)) => bitmap_id(parent)? != bitmap_id(module)?,
539 } {
540 exposed_modules_imported.insert(module);
545 }
546 if parent_info.is_some_and(|(_, r)| {
547 matches!(
548 r.binding_usage.export,
549 ExportUsage::All
550 | ExportUsage::PartialNamespaceObject(_)
551 | ExportUsage::Passthrough { .. }
552 )
553 }) {
554 exposed_modules_namespace.insert(module);
557 }
558 Ok(())
559 },
560 false,
561 false,
562 )?;
563
564 drop(inner_span);
565 let inner_span = tracing::info_span!("reconciliation").entered();
566 while let Some((_, common_occurrences)) = lists_reverse_indices.pop() {
567 if common_occurrences.len() < 2 {
568 continue;
570 }
571 let first_occurrence = &common_occurrences[0];
575
576 let mut common_length = 2;
578 loop {
579 let m = lists[first_occurrence.chunk_group][first_occurrence.list]
580 .get(first_occurrence.entry + common_length - 1);
581 if m.is_some()
582 && common_occurrences.iter().skip(1).all(
583 |ListOccurrence {
584 chunk_group,
585 list,
586 entry,
587 }| {
588 lists[*chunk_group][*list].get(*entry + common_length - 1) == m
589 },
590 )
591 {
592 common_length += 1;
593 continue;
594 }
595
596 common_length -= 1;
598 break;
599 }
600
601 let common_list = lists[first_occurrence.chunk_group][first_occurrence.list]
606 [first_occurrence.entry..first_occurrence.entry + common_length]
607 .to_vec();
608
609 let common_list_index = lists[LISTS_COMMON_IDX].len();
610 lists[LISTS_COMMON_IDX].push(common_list.clone());
611
612 for (i, &m) in common_list.iter().enumerate().skip(1) {
615 let occurrences = lists_reverse_indices
616 .get_mut(&m)
617 .context("every module should have occurrences")?;
618 for common_occurrence in &common_occurrences {
619 let removed = occurrences.swap_remove(&ListOccurrence {
620 chunk_group: common_occurrence.chunk_group,
621 list: common_occurrence.list,
622 entry: common_occurrence.entry + i,
623 });
624 debug_assert!(removed);
625 }
626 occurrences.insert(ListOccurrence {
627 chunk_group: LISTS_COMMON_IDX,
628 list: common_list_index,
629 entry: i,
630 });
631 }
632
633 for common_occurrence in &common_occurrences {
634 let list = &mut lists[common_occurrence.chunk_group][common_occurrence.list];
635 let after_list = list.split_off(common_occurrence.entry + common_length);
636 list.truncate(common_occurrence.entry);
637 let before_list = &*list;
638
639 {
646 let before_list =
647 FxHashSet::from_iter(before_list.iter().map(|m| ResolvedVc::upcast(*m)));
648 let common_list =
649 FxHashSet::from_iter(common_list.iter().map(|m| ResolvedVc::upcast(*m)));
650 let after_list =
651 FxHashSet::from_iter(after_list.iter().map(|m| ResolvedVc::upcast(*m)));
652
653 let references_from_before = before_list
654 .iter()
655 .filter_map(|m| intra_group_references.get(m))
656 .flatten()
657 .copied()
658 .filter(|m| common_list.contains(m) || after_list.contains(m))
659 .collect::<FxHashSet<_>>();
660 let references_from_common = common_list
661 .iter()
662 .filter_map(|m| intra_group_references.get(m))
663 .flatten()
664 .filter(|m| before_list.contains(m) || after_list.contains(m))
665 .collect::<FxHashSet<_>>();
666 let references_from_after = after_list
667 .iter()
668 .filter_map(|m| intra_group_references.get(m))
669 .flatten()
670 .copied()
671 .filter(|m| before_list.contains(m) || common_list.contains(m))
672 .collect::<FxHashSet<_>>();
673
674 let modules_to_expose = before_list
675 .iter()
676 .chain(common_list.iter())
677 .chain(after_list.iter())
678 .copied()
679 .filter(|m| {
680 references_from_before.contains(m)
681 || references_from_common.contains(m)
682 || references_from_after.contains(m)
683 });
684
685 exposed_modules_imported.extend(modules_to_expose);
686 }
687
688 if !after_list.is_empty() {
691 let after_index = lists[LISTS_COMMON_IDX].len();
692 lists[LISTS_COMMON_IDX].push(after_list.clone());
693 for (i, &m) in after_list.iter().enumerate() {
694 let Some(occurrences) = lists_reverse_indices.get_mut(&m) else {
695 bail!("Couldn't find module in reverse list");
696 };
697
698 let removed = occurrences.swap_remove(&ListOccurrence {
699 chunk_group: common_occurrence.chunk_group,
700 list: common_occurrence.list,
701 entry: common_occurrence.entry + common_length + i,
702 });
703 debug_assert!(removed);
704
705 occurrences.insert(ListOccurrence {
706 chunk_group: LISTS_COMMON_IDX,
707 list: after_index,
708 entry: i,
709 });
710 }
711 }
712 }
713 }
714
715 let lists = lists.into_iter().flatten().collect::<FxHashSet<_>>();
717
718 drop(inner_span);
719 let inner_span = tracing::info_span!("merging");
720 let result = lists
722 .into_iter()
723 .map(async |list| {
724 if list.len() < 2 {
725 return Ok(None);
727 }
728
729 let list_set = list
730 .iter()
731 .map(|&m| ResolvedVc::upcast::<Box<dyn Module>>(m))
732 .collect::<FxIndexSet<_>>();
733
734 let entry_points = list
735 .iter()
736 .filter(|m| {
737 intra_group_references_rev
738 .get(&ResolvedVc::upcast(**m))
739 .is_none_or(|refs| refs.is_disjoint(&list_set))
740 })
741 .map(|m| **m)
742 .collect::<Vec<_>>();
743 debug_assert_ne!(entry_points.len(), 0);
744
745 let list_exposed = list
746 .iter()
747 .map(|&m| {
748 (
749 m,
750 if exposed_modules_imported.contains(&ResolvedVc::upcast(m)) {
751 MergeableModuleExposure::External
752 } else if exposed_modules_namespace.contains(&ResolvedVc::upcast(m)) {
753 MergeableModuleExposure::Internal
754 } else {
755 MergeableModuleExposure::None
756 },
757 )
758 })
759 .collect::<Vec<_>>();
760
761 let entry = *list.last().unwrap();
762 let result = entry
763 .merge(
764 MergeableModulesExposed::interned(list_exposed),
765 MergeableModules::interned(entry_points),
766 )
767 .to_resolved()
768 .await?;
769
770 let list_len = list.len();
771 Ok(Some((
772 ResolvedVc::upcast::<Box<dyn Module>>(entry),
773 result,
774 list.into_iter()
775 .take(list_len - 1)
776 .map(ResolvedVc::upcast::<Box<dyn Module>>)
777 .collect::<Vec<_>>(),
778 )))
779 })
780 .try_join()
781 .instrument(inner_span)
782 .await?;
783
784 #[allow(clippy::type_complexity)]
785 let mut replacements: FxHashMap<
786 ResolvedVc<Box<dyn Module>>,
787 Option<ResolvedVc<Box<dyn ChunkableModule>>>,
788 > = Default::default();
789 #[allow(clippy::type_complexity)]
790 let mut replacements_to_original: FxHashMap<
791 ResolvedVc<Box<dyn Module>>,
792 ResolvedVc<Box<dyn Module>>,
793 > = Default::default();
794 let mut merged_groups = 0;
795 let mut included_modules = 0;
796
797 for (original, replacement, replacement_included) in result.into_iter().flatten() {
798 replacements.insert(original, Some(replacement));
799 replacements_to_original.insert(ResolvedVc::upcast(replacement), original);
800 merged_groups += 1;
801 included_modules += replacement_included.len();
802 for included in replacement_included {
803 replacements.insert(included, None);
804 }
805 }
806
807 span.record("merged_groups", merged_groups);
808 span.record("included_modules", included_modules);
809
810 Ok(MergedModuleInfo {
811 replacements: ResolvedVc::cell(replacements),
812 replacements_to_original: ResolvedVc::cell(replacements_to_original),
813 }
814 .cell())
815 }
816 .instrument(span_outer)
817 .await
818}
819
820#[cfg(test)]
821mod tests {
822 use roaring::RoaringBitmap;
823
824 use super::*;
825
826 fn bitmap(bits: &[u32]) -> RoaringBitmapWrapper {
827 RoaringBitmapWrapper(bits.iter().copied().collect::<RoaringBitmap>())
828 }
829
830 #[test]
831 fn intern_bitmaps_gives_equal_bitmaps_equal_ids() {
832 let a = bitmap(&[1, 5, 100_000]);
833 let a_reordered = bitmap(&[100_000, 1, 5]);
834 let b = bitmap(&[1, 5]);
835 let empty = bitmap(&[]);
836 let empty2 = bitmap(&[]);
837 let entries = [
838 ("a", &a),
839 ("b", &b),
840 ("a_reordered", &a_reordered),
841 ("empty", &empty),
842 ("empty2", &empty2),
843 ];
844
845 let (ids, distinct) = intern_bitmaps(entries.iter().copied(), entries.len());
846
847 assert_eq!(distinct, 3);
848 assert_eq!(ids.len(), entries.len());
849 assert_eq!(ids["a"], ids["a_reordered"]);
850 assert_eq!(ids["empty"], ids["empty2"]);
851 assert_ne!(ids["a"], ids["b"]);
852 assert_ne!(ids["a"], ids["empty"]);
853 assert_ne!(ids["b"], ids["empty"]);
854 let mut all = ids.values().copied().collect::<Vec<_>>();
856 all.sort_unstable();
857 all.dedup();
858 assert_eq!(all, vec![0, 1, 2]);
859 }
860}