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 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 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 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
138pub 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 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 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; }
184 }
185 reduced.insert(*dep);
186 }
187 reduced
188 }
189
190 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 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 module.imports.extend(top_shared_deps.clone());
233 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 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 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 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 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 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); 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 reachability_graph.print("no_inline_fn", &info, &dep_graph);
396 assert_eq!(reachability_graph.len(), 7); 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 assert_eq!(reachability_graph.len(), 8); }
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 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, 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 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 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 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 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 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 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 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}