Skip to main content

wamex_cli/analysis/
dep_graph.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, VecDeque},
3    fmt::Debug,
4};
5
6use crate::{
7    analysis::{self},
8    index::{Id, IdMap, SymbolId},
9};
10
11pub type DepSet<T = SymbolId> = BTreeSet<T>;
12pub type DepMiniSet<T = SymbolId> = wamex_types::map_vec::MiniSet<T>;
13
14#[derive(Clone, Default)]
15struct SymbolStructure {
16    pub parents: DepMiniSet,
17    pub childs: DepMiniSet,
18}
19#[derive(Clone, Default)]
20pub struct DepGraph {
21    // TODO: use VecMap kind of structure for better performance
22    nodes: IdMap<SymbolId, SymbolStructure>,
23}
24impl DepGraph {
25    pub fn new() -> Self {
26        Self {
27            nodes: IdMap::new(),
28        }
29    }
30
31    #[cfg(test)]
32    pub(crate) fn insert_child(&mut self, parent: SymbolId, child: SymbolId) {
33        let parent_struct = self.nodes.entry(parent).or_insert_with(Default::default);
34        parent_struct.childs.insert(child);
35
36        let child_struct = self.nodes.entry(child).or_insert_with(Default::default);
37        child_struct.parents.insert(parent);
38    }
39
40    pub fn get_children(&self, key: SymbolId) -> Option<&DepMiniSet> {
41        self.nodes.get(key).map(|s| &s.childs)
42    }
43    pub fn get_parents(&self, key: SymbolId) -> Option<&DepMiniSet> {
44        self.nodes.get(key).map(|s| &s.parents)
45    }
46    pub fn iter_childs(&self) -> impl Iterator<Item = (SymbolId, &DepMiniSet)> {
47        self.nodes.iter().map(|(k, v)| (k, &v.childs))
48    }
49}
50
51impl Debug for DepGraph {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        for (node, deps) in self.iter_childs() {
54            if deps.is_empty() {
55                writeln!(f, "{node:?} -> <no deps>")?;
56                continue;
57            }
58            write!(f, "{node:?} -> ")?;
59            let mut deps = deps.iter();
60            if let Some(dep) = deps.next() {
61                write!(f, "{dep:?}")?;
62            }
63
64            for dep in deps {
65                write!(f, " & {dep:?}")?;
66            }
67            writeln!(f)?;
68        }
69        Ok(())
70    }
71}
72
73#[derive(Debug, Clone)]
74pub struct NamedGraph<Id> {
75    pub module: Id,
76    pub reachable: DepSet,
77
78    /// This field is hidden, because it is output parameter of `calculate_shared_modules`
79    imports: DepMiniSet,
80}
81
82impl<Id> NamedGraph<Id> {
83    pub fn new(module: Id, reachable: DepSet) -> Self {
84        Self {
85            module,
86            reachable,
87            imports: DepMiniSet::new(),
88        }
89    }
90    pub fn imports(&self) -> &DepMiniSet {
91        &self.imports
92    }
93}
94
95#[derive(Debug, Clone)]
96pub struct SharedEntries<Id> {
97    pub module_names: Vec<Id>,
98    pub shared_deps: DepSet,
99    pub exports: DepMiniSet,
100    pub imports: DepMiniSet,
101}
102
103pub fn get_dependencies(info: &analysis::ModuleInfo) -> anyhow::Result<DepGraph> {
104    let mut deps = DepGraph::new();
105
106    // TypeIndexLeb relocations index is not in symbol index space.
107    let non_type_index = |entry: &&wasmparser::RelocationEntry| {
108        use wasmparser::RelocationType;
109        !matches!(entry.ty, RelocationType::TypeIndexLeb)
110    };
111    let is_fn_or_data = |id: &SymbolId| info.symbols.is_function(*id) || info.symbols.is_data(*id);
112
113    for (id, child) in info.symbols.iter() {
114        if !is_fn_or_data(&id) {
115            continue;
116        }
117        let childs = DepMiniSet::from_iter(
118            child
119                .relocs
120                .iter()
121                .filter(non_type_index)
122                .map(|entry| Id::from_index(entry.index))
123                .filter_map(|index| info.symbols.as_duplicate_mapped(index).or(Some(index)))
124                .filter(is_fn_or_data),
125        );
126
127        for child_id in &childs {
128            let child_struct = deps.nodes.entry(*child_id).or_insert_with(Default::default);
129            child_struct.parents.insert(id);
130        }
131
132        deps.nodes.entry(id).or_insert_with(Default::default).childs = childs;
133    }
134
135    Ok(deps)
136}
137
138// traverse the dep graph starting from roots and return all reachable nodes
139pub fn find_reachable_deps(deps: &DepGraph, roots: &DepSet) -> DepSet {
140    let mut queue: VecDeque<_> = roots.iter().copied().collect();
141    let mut seen = DepSet::new();
142
143    while let Some(node) = queue.pop_front() {
144        if !seen.insert(node) {
145            continue;
146        }
147
148        let Some(children) = deps.get_children(node) else {
149            continue;
150        };
151        for child in children {
152            queue.push_back(*child);
153        }
154    }
155    seen
156}
157
158impl<Id> NamedGraph<Id> {
159    /// Collect list of modules that owns a given dep node
160    /// Returns a map of dep node to set of module ids that owns it
161    fn collect_visited_by(modules: &[NamedGraph<Id>]) -> BTreeMap<SymbolId, DepSet<usize>> {
162        let mut visited_by: BTreeMap<SymbolId, DepSet<usize>> = BTreeMap::new();
163        for (module_id, module) in modules.iter().enumerate() {
164            for dep in module.reachable.iter() {
165                visited_by.entry(*dep).or_default().insert(module_id);
166            }
167        }
168        visited_by
169    }
170
171    /// List only shared entries that have parents in module entries.
172    /// This will collect nodes that module entries imports from shared entries.
173    pub fn reduce_shared_entries(
174        shared_entries: &DepSet,
175        module_entries: &DepSet,
176        graph: &DepGraph,
177    ) -> DepSet {
178        let mut reduced = DepSet::new();
179        for dep in shared_entries {
180            if let Some(parent) = graph.get_parents(*dep) {
181                if !parent.iter().any(|p| module_entries.contains(p)) {
182                    continue; // skip if all parents are also in shared entries
183                }
184            }
185            reduced.insert(*dep);
186        }
187        reduced
188    }
189
190    /// Calculate shared entries between modules.
191    /// Returns a vector of SharedEntries, each containing a list of module names and shared dependencies.
192    /// Shared dependencies are those that are reachable from multiple modules.
193    pub fn calculate_shared_modules(
194        modules: &mut [NamedGraph<Id>],
195        graph: &DepGraph,
196    ) -> Vec<SharedEntries<Id>>
197    where
198        Id: Clone + Ord + Debug,
199    {
200        //TODO: use bitset as key instead
201        let mut shared_entries: BTreeMap<Vec<usize>, DepSet> = BTreeMap::new();
202
203        for m in modules.iter() {
204            debug_assert!(m.imports.is_empty(), "Linked nodes is output parameter");
205        }
206
207        let visited_by = Self::collect_visited_by(modules);
208
209        for (dep, owner_modules) in visited_by {
210            if owner_modules.len() > 1 {
211                for module_id in &owner_modules {
212                    let module = &mut modules[*module_id];
213                    module.reachable.remove(&dep);
214                }
215                let mut owner_modules: Vec<usize> = owner_modules.into_iter().collect();
216                owner_modules.sort_unstable();
217                shared_entries.entry(owner_modules).or_default().insert(dep);
218            }
219        }
220
221        let mut result = Vec::new();
222        for (module_ids, shared_deps) in shared_entries {
223            let mut module_names = Vec::new();
224            let mut shared_exports = DepMiniSet::new();
225
226            for module_id in module_ids {
227                let module = &mut modules[module_id];
228
229                let top_shared_deps =
230                    Self::reduce_shared_entries(&shared_deps, &module.reachable, graph);
231                // imports
232                module.imports.extend(top_shared_deps.clone());
233                // exports
234                shared_exports.extend_and_resort(top_shared_deps.into_iter());
235                module_names.push(module.module.clone());
236            }
237            result.push(SharedEntries {
238                module_names,
239                shared_deps,
240                exports: shared_exports,
241                imports: DepMiniSet::new(),
242            });
243        }
244
245        // For each dep -> child if child is not found in deps: add it to linked_nodes
246        // Imports
247        for shared in result.iter_mut() {
248            let new_imports = shared
249                .shared_deps
250                .iter()
251                .filter_map(|d| graph.get_children(*d))
252                .flatten()
253                .filter(|child| !shared.shared_deps.contains(child))
254                .copied();
255
256            let mut new_exports = Vec::new();
257            for dep in &shared.shared_deps {
258                if let Some(parents) = graph.get_parents(*dep) {
259                    for parent in parents {
260                        if !shared.shared_deps.contains(parent) {
261                            new_exports.push(*dep);
262                        }
263                    }
264                }
265            }
266
267            shared.imports.extend_and_resort(new_imports);
268            shared.exports.extend_and_resort(new_exports);
269        }
270        // TODO For export imports we can just build a map (dep -> owner) then
271        // map((dep-> owner), dep -> (children, parents) ) -> (dep -> imports/exports)
272
273        result.sort_by(|left, right| left.module_names.cmp(&right.module_names));
274        result
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use std::{fs::File, io::Write};
281
282    use lazy_static::lazy_static;
283
284    use crate::{
285        analysis::{
286            self,
287            debug::print_deps_inner,
288            dep_graph::{DepGraph, DepSet},
289            symbols::SymbolKind,
290            testing,
291        },
292        index::{Id, SymbolId},
293    };
294
295    trait DepListExt {
296        fn check_unreachable(&self, other: &DepSet) -> bool;
297        fn print(&self, title: &str, info: &analysis::ModuleInfo, graph: &DepGraph);
298    }
299    impl DepListExt for DepSet {
300        // Check that self list of deps does not contain any nodes from other list
301        fn check_unreachable(&self, other: &DepSet) -> bool {
302            for dep in other {
303                if self.contains(dep) {
304                    log::warn!("Unreachable dep {dep:?} found in deps");
305                    return false;
306                }
307            }
308            true
309        }
310        fn print(&self, title: &str, info: &analysis::ModuleInfo, graph: &DepGraph) {
311            print_deps_inner(title, info, self, graph);
312        }
313    }
314    // checkout test-data/simple-graph crate at root (just keep wasm in case rustc changes)
315    const WASM_FILE: &[u8] = include_bytes!("../../test-data/simple_graph.wasm");
316
317    #[test]
318    fn load_dep_graph() {
319        let info = analysis::ModuleInfo::from_wasm_bytes(&WASM_FILE).unwrap();
320        let dep_graph = super::get_dependencies(&info).unwrap();
321
322        let format_dep = |dep: SymbolId| {
323            let symbol = info.symbols.get(dep).unwrap();
324            let name = &symbol.name;
325            match symbol.kind {
326                SymbolKind::Func { input_id } => {
327                    format!("func[{input_id}] <{name:?}>")
328                }
329                SymbolKind::DataDefined {
330                    segment_id,
331                    offset,
332                    length,
333                } => {
334                    format!("data[{segment_id}:{offset}:{length}] <{name:?}>")
335                }
336                _ => panic!("unexpected symbol kind"),
337            }
338        };
339
340        for (node, deps) in dep_graph.iter_childs() {
341            println!("node: {node}", node = format_dep(node));
342            for dep in deps {
343                println!("  =>{dep}", dep = format_dep(*dep));
344            }
345        }
346
347        let no_inline_fn = info.find_function_id_by_name("no_inline_fn").unwrap();
348        let no_inline_fn_sym = info.symbols.get_function_symbol(no_inline_fn).unwrap();
349
350        let deps = dep_graph.get_children(no_inline_fn_sym).unwrap();
351        let func_deps: Vec<_> = deps
352            .iter()
353            .filter(|dep| info.symbols.is_function(**dep))
354            .collect();
355        let data_deps: Vec<_> = deps
356            .iter()
357            .filter(|dep| info.symbols.is_data(**dep))
358            .collect();
359
360        assert_eq!(func_deps.len(), 3);
361        // no_inline_fn is really inline data, but keep method call for "side effect"
362        assert_eq!(data_deps.len(), 3);
363
364        let indirect_fn = info.find_function_id_by_name("indirect_fn").unwrap();
365        let indirect_fn_sym = info.symbols.get_function_symbol(indirect_fn).unwrap();
366
367        let deps = dep_graph.get_children(indirect_fn_sym).unwrap();
368        assert_eq!(deps.len(), 1); // only dep on switchtable
369        let switch_table = *deps.iter().next().unwrap();
370        assert!(matches!(
371            info.symbols.get(switch_table).unwrap().kind,
372            SymbolKind::DataDefined { .. }
373        ));
374        let fns = dep_graph.get_children(switch_table).unwrap();
375
376        assert_eq!(fns.len(), 3);
377    }
378
379    #[test]
380    fn reachablity_graph() {
381        let info = analysis::ModuleInfo::from_wasm_bytes(&WASM_FILE).unwrap();
382        let dep_graph = super::get_dependencies(&info).unwrap();
383
384        let no_inline_fn = info.find_function_id_by_name("no_inline_fn").unwrap();
385        let no_inline_fn_sym = info.symbols.get_function_symbol(no_inline_fn).unwrap();
386
387        let reachability_graph =
388            super::find_reachable_deps(&dep_graph, &DepSet::from_iter([no_inline_fn_sym]));
389        // no_inline_fn -> data1
390        //              -> data2
391        //              -> data3
392        //              -> func1 -> data1
393        //              -> func2 -> data2
394        //              -> func3 -> data3
395        reachability_graph.print("no_inline_fn", &info, &dep_graph);
396        assert_eq!(reachability_graph.len(), 7); // root +  3 data + 3 funcs
397
398        let indirect_fn = info.find_function_id_by_name("indirect_fn").unwrap();
399        let reachability_graph = super::find_reachable_deps(
400            &dep_graph,
401            &DepSet::from_iter([info.symbols.get_function_symbol(indirect_fn).unwrap()]),
402        );
403        reachability_graph.print("indirect_fn", &info, &dep_graph);
404        // almost same count, but indirect_fn has more deep graph and switchtable
405        // indirect_fn -> switchtable -> func1 -> data1
406        //                             -> func2 -> data2
407        //                             -> func3 -> data3
408        assert_eq!(reachability_graph.len(), 8); // root + <switchtable> +  3 data + 3 funcs
409    }
410
411    lazy_static! {
412        static ref TEST_GRAPH: DepGraph = testing::parse_deps(
413            r#"
414            1 -> 2 & 4 -> 5 & 7 -> 8
415            11 -> 4 & 12
416            "#
417        )
418        .unwrap();
419    }
420    #[test]
421    fn test_unique_nodes() {
422        let graph = TEST_GRAPH.clone();
423        let modules = vec![
424            super::NamedGraph::new(
425                "module1",
426                super::find_reachable_deps(&graph, &testing::uniq_nodes("1").unwrap()),
427            ),
428            super::NamedGraph::new(
429                "module2",
430                super::find_reachable_deps(&graph, &testing::uniq_nodes("11").unwrap()),
431            ),
432        ];
433
434        let first_module = &modules[0];
435        let first_graph = testing::uniq_nodes("1 & 2 & 4 & 5 & 7 & 8").unwrap();
436
437        assert_eq!(first_module.reachable, first_graph);
438        assert!(
439            first_module
440                .reachable
441                .check_unreachable(&testing::uniq_nodes("11 & 12").unwrap())
442        );
443
444        let second_module = &modules[1];
445        let second_graph = testing::uniq_nodes("11 & 12 & 4 & 5 & 7 & 8").unwrap();
446        assert_eq!(second_module.reachable, second_graph);
447
448        assert!(
449            second_module
450                .reachable
451                .check_unreachable(&testing::uniq_nodes("1 & 2 & 3").unwrap())
452        );
453    }
454
455    #[test]
456    fn test_shared_entries() {
457        let graph = TEST_GRAPH.clone();
458        let mut modules = vec![
459            super::NamedGraph::new(
460                "module1",
461                super::find_reachable_deps(&graph, &testing::uniq_nodes("1").unwrap()),
462            ),
463            super::NamedGraph::new(
464                "module2",
465                super::find_reachable_deps(&graph, &testing::uniq_nodes("11").unwrap()),
466            ),
467        ];
468
469        let shared_entries = super::NamedGraph::calculate_shared_modules(&mut modules, &graph);
470
471        assert_eq!(shared_entries.len(), 1);
472        assert_eq!(shared_entries[0].module_names, vec!["module1", "module2"]);
473        assert_eq!(
474            shared_entries[0].shared_deps,
475            testing::uniq_nodes("5 & 8 & 4 & 7").unwrap()
476        );
477
478        // Test that top_most_dep contains only 4
479        // It is top-most because it doesn't depend on other shared dependencies
480        // 7, 5 and 8 are not top-most because 4 -> 5 & 7 and 7 -> 8
481        for module in &modules {
482            assert_eq!(module.imports, testing::uniq_nodes("4").unwrap());
483        }
484    }
485
486    #[test]
487    fn test_multiple_shared_deps() {
488        let input = r#"
489        1 -> 102 & 11 & 4 -> 115
490        11 -> 112 -> 12 & 4 & 7 -> 118
491        10 -> 111 & 7
492        20 -> 4 & 7
493        "#;
494        let mut modules = vec![
495            super::NamedGraph::new(
496                "module1",
497                super::find_reachable_deps(
498                    &testing::parse_deps(input).unwrap(),
499                    &testing::uniq_nodes("1").unwrap(),
500                ),
501            ),
502            super::NamedGraph::new(
503                "module2",
504                super::find_reachable_deps(
505                    &testing::parse_deps(input).unwrap(),
506                    &testing::uniq_nodes("10").unwrap(),
507                ),
508            ),
509            super::NamedGraph::new(
510                "module3",
511                super::find_reachable_deps(
512                    &testing::parse_deps(input).unwrap(),
513                    &testing::uniq_nodes("20").unwrap(),
514                ),
515            ),
516        ];
517
518        let shared_entries = super::NamedGraph::calculate_shared_modules(
519            &mut modules,
520            &testing::parse_deps(input).unwrap(),
521        );
522
523        assert_eq!(shared_entries.len(), 2);
524        assert_eq!(
525            shared_entries[0].module_names,
526            vec!["module1", "module2", "module3"]
527        );
528        assert_eq!(
529            shared_entries[0].shared_deps,
530            testing::uniq_nodes("118 & 7").unwrap()
531        );
532
533        assert_eq!(modules[1].imports, testing::uniq_nodes("7").unwrap());
534        assert_eq!(shared_entries[1].module_names, vec!["module1", "module3"]);
535        assert_eq!(
536            shared_entries[1].shared_deps, // 7 and children are stored in [m1,m2,m3] shared deps
537            testing::uniq_nodes("4 & 115").unwrap()
538        );
539        for module in &[&modules[0], &modules[2]] {
540            assert_eq!(module.imports, testing::uniq_nodes("4 & 7").unwrap());
541        }
542    }
543
544    #[test]
545    fn test_recursive_shared_deps() {
546        // F4 is parent of F7 which call F4
547        let input = r#"
548        1 -> 102 & 4 -> 105 & 7 -> 108 & 4
549        10 -> 111 -> 4
550        "#;
551        let mut modules = vec![
552            super::NamedGraph::new(
553                "module1",
554                super::find_reachable_deps(
555                    &testing::parse_deps(input).unwrap(),
556                    &testing::uniq_nodes("1").unwrap(),
557                ),
558            ),
559            super::NamedGraph::new(
560                "module2",
561                super::find_reachable_deps(
562                    &testing::parse_deps(input).unwrap(),
563                    &testing::uniq_nodes("10").unwrap(),
564                ),
565            ),
566        ];
567
568        let shared_entries = super::NamedGraph::calculate_shared_modules(
569            &mut modules,
570            &testing::parse_deps(input).unwrap(),
571        );
572
573        assert_eq!(shared_entries.len(), 1);
574        assert_eq!(shared_entries[0].module_names, vec!["module1", "module2"]);
575        assert_eq!(
576            shared_entries[0].shared_deps,
577            testing::uniq_nodes("4 & 105 & 7 & 108").unwrap()
578        );
579        for module in &modules {
580            assert_eq!(module.imports, testing::uniq_nodes("4").unwrap());
581        }
582    }
583
584    #[test]
585    fn test_shared_deps_reduced() {
586        let source = r#"
587    899  -> 307 & 912
588    1358 -> 1417 & 1418 & 4759
589    124  -> 4759
590    307  -> 308 & 124
591    912  -> 1358
592    1    -> 307
593            "#;
594        let graph = testing::parse_deps(source).unwrap();
595
596        let mut modules = vec![
597            super::NamedGraph::new(
598                "main",
599                super::find_reachable_deps(&graph, &testing::uniq_nodes("1").unwrap()),
600            ),
601            super::NamedGraph::new(
602                "split_string_from_static",
603                super::find_reachable_deps(&graph, &testing::uniq_nodes("899").unwrap()),
604            ),
605        ];
606
607        dbg!(&modules);
608
609        let shared_entries = super::NamedGraph::calculate_shared_modules(&mut modules, &graph);
610
611        dbg!(&shared_entries);
612        let node = Id::from_index(1417);
613
614        assert!(modules[1].reachable.contains(&node));
615
616        let roots = [node].into_iter().collect();
617        let child = super::find_reachable_deps(&graph, &roots);
618
619        assert!(!child.is_empty());
620        // make sure that all childs of specific node (that known to be in module 2) are included or linked
621        for node in child {
622            dbg!(node);
623            let direct_dep = modules[1].reachable.contains(&node);
624            let linked_dep = modules[1].imports.contains(&node);
625
626            assert!(direct_dep || linked_dep);
627        }
628    }
629
630    const INPUT: &str = r#"
631    1358 -> 1359 & 1366 & 1417
632    4453 -> 4800 & 4759
633    912  -> 1358
634    4452 -> 4453
635    4630 -> 4452 & 4464
636    4759 -> 4671
637    1417 -> 1418
638    1418 -> 4759
639    899  -> 912 & 307 & 916
640    4671 -> 4663
641                "#;
642    #[test]
643    fn test_deps_in_shared_conflict() {
644        assert!(test_deps_in_shared_conflict_impl(INPUT));
645    }
646
647    #[test]
648    #[ignore = "takes too long time"]
649    fn test_reduce_deps_in_shared_conflict() {
650        reduce(INPUT, test_deps_in_shared_conflict_impl);
651    }
652
653    fn test_deps_in_shared_conflict_impl(source: &str) -> bool {
654        let graph = testing::parse_deps(source).unwrap();
655
656        let mut modules = vec![
657            super::NamedGraph::new(
658                "main",
659                super::find_reachable_deps(&graph, &testing::uniq_nodes("4630").unwrap()),
660            ),
661            super::NamedGraph::new(
662                "split_static_str",
663                super::find_reachable_deps(&graph, &testing::uniq_nodes("897").unwrap()),
664            ),
665            super::NamedGraph::new(
666                "split_string_from_static",
667                super::find_reachable_deps(&graph, &testing::uniq_nodes("899").unwrap()),
668            ),
669        ];
670
671        let _shared_entries = super::NamedGraph::calculate_shared_modules(&mut modules, &graph);
672
673        let node = Id::from_index(1417);
674
675        if !modules[2].reachable.contains(&node) {
676            return true;
677        }
678
679        let children = graph.get_children(node).unwrap().clone();
680        assert!(!children.is_empty());
681        // make sure that all childs of specific node (that known to be in module 2) are included or linked
682        for node in children {
683            dbg!(node);
684            let direct_dep = modules[2].reachable.contains(&node);
685            let linked_dep = modules[2].imports.contains(&node);
686
687            if !direct_dep && !linked_dep {
688                return false;
689            }
690        }
691        return true;
692    }
693
694    #[test]
695    fn test_of_shared_dep_of_shared() {
696        let source = r#"
697    1 ->  2 -> 3 -> 4
698    11 -> 12 -> 13 -> 14
699    21 -> 22 -> 23 -> 24
700    31 -> 32 -> 33 -> 34
701    100 -> 101 -> 102
702    22 -> 100 & 201
703    32 -> 100 & 201
704    12 -> 101 & 301
705    "#;
706        let graph = testing::parse_deps(source).unwrap();
707
708        let mut modules = vec![
709            super::NamedGraph::new(
710                "mod1",
711                super::find_reachable_deps(&graph, &testing::uniq_nodes("1").unwrap()),
712            ),
713            super::NamedGraph::new(
714                "mod2",
715                super::find_reachable_deps(&graph, &testing::uniq_nodes("11").unwrap()),
716            ),
717            super::NamedGraph::new(
718                "mod3",
719                super::find_reachable_deps(&graph, &testing::uniq_nodes("21").unwrap()),
720            ),
721            super::NamedGraph::new(
722                "mod4",
723                super::find_reachable_deps(&graph, &testing::uniq_nodes("31").unwrap()),
724            ),
725        ];
726
727        // So 1 not share anything
728        // 11, 21, 31 are sharing 101
729        // But 21 & 31 are sharing also 101 parent 100
730        let shared_entries = super::NamedGraph::calculate_shared_modules(&mut modules, &graph);
731        assert_eq!(shared_entries.len(), 2);
732        let first = &shared_entries[0];
733        assert_eq!(
734            first.module_names,
735            &["mod2".to_string(), "mod3".to_string(), "mod4".to_string()]
736        );
737
738        // 101 exported, but 101 and 102 are both defined
739        assert!(first.exports.contains(&Id::from_index(101)));
740        assert!(first.shared_deps.contains(&Id::from_index(101)));
741        assert!(first.shared_deps.contains(&Id::from_index(102)));
742
743        let second = &shared_entries[1];
744        assert_eq!(
745            second.module_names,
746            &["mod3".to_string(), "mod4".to_string()]
747        );
748
749        dbg!(&second);
750        // 100 are exported and defined (201 also defined, but not interesting here)
751        assert!(!second.imports.contains(&Id::from_index(100)));
752        assert!(second.exports.contains(&Id::from_index(100)));
753        assert!(second.shared_deps.contains(&Id::from_index(100)));
754        // 101 are imported only
755        assert!(second.imports.contains(&Id::from_index(101)));
756        assert!(!second.exports.contains(&Id::from_index(101)));
757        assert!(!second.shared_deps.contains(&Id::from_index(101)));
758    }
759
760    fn reduce(source: &str, test: impl Fn(&str) -> bool) {
761        let mut prefix = source.lines().collect::<Vec<_>>();
762
763        let mut suffix = String::new();
764
765        let max_iter = 4000;
766        for _ in 0..max_iter {
767            let mut new_prefix = prefix.clone();
768            let Some(removed) = new_prefix.pop() else {
769                break;
770            };
771
772            let mut file = File::create("./reduced.txt").unwrap();
773            let mut source = new_prefix.join("\n");
774            source.push_str(&suffix);
775
776            if test(&source) {
777                println!("removed: {}", removed);
778                suffix = format!("\n{}{}", removed, suffix);
779            } else {
780                file.write_all(format!("{}\n", source).as_bytes()).unwrap();
781                file.flush().unwrap();
782                prefix = new_prefix;
783            }
784        }
785
786        let mut source = prefix.join("\n");
787        source.push_str(&suffix);
788        eprintln!("Reduced to:\n{}", source);
789    }
790}