1use crate::graph::{DAG, RenderMode};
4use alloc::{string::String, vec, vec::Vec};
5use core::fmt::Write;
6
7pub(crate) const V_LINE: char = '│';
9pub(crate) const H_LINE: char = '─';
10pub(crate) const ARROW_DOWN: char = '↓';
11pub(crate) const ARROW_RIGHT: char = '→';
12pub(crate) const CYCLE_ARROW: char = '⇄'; pub(crate) const CORNER_DR: char = '└'; pub(crate) const CORNER_DL: char = '┘'; pub(crate) const TEE_DOWN: char = '┬'; pub(crate) const TEE_UP: char = '┴'; pub(crate) const CORNER_UR: char = '┌'; pub(crate) const CORNER_UL: char = '┐'; impl<'a> DAG<'a> {
23 pub fn render(&self) -> String {
39 let mut buf = String::with_capacity(self.estimate_size());
40 self.render_to(&mut buf);
41 buf
42 }
43
44 pub fn render_to(&self, output: &mut String) {
61 if self.nodes.is_empty() {
62 output.push_str("Empty DAG");
63 return;
64 }
65
66 if self.has_cycle() {
68 self.render_cycle(output);
69 return;
70 }
71
72 let mode = match self.render_mode {
74 RenderMode::Auto => {
75 if self.is_simple_chain() {
76 RenderMode::Horizontal
77 } else {
78 RenderMode::Vertical
79 }
80 }
81 other => other,
82 };
83
84 match mode {
85 RenderMode::Horizontal => self.render_horizontal(output),
86 RenderMode::Vertical | RenderMode::Auto => self.render_vertical(output),
87 }
88 }
89
90 fn render_cycle(&self, output: &mut String) {
92 writeln!(output, "⚠️ CYCLE DETECTED - Not a valid DAG").ok();
93 writeln!(output).ok();
94
95 if let Some(cycle_nodes) = self.find_cycle_path() {
97 writeln!(output, "Cyclic dependency chain:").ok();
98
99 for (i, node_id) in cycle_nodes.iter().enumerate() {
100 if let Some(&(id, label)) = self.nodes.iter().find(|(nid, _)| nid == node_id) {
101 self.write_node(output, id, label);
102
103 if i < cycle_nodes.len() - 1 {
104 write!(output, " → ").ok();
105 } else {
106 if let Some(&(first_id, first_label)) =
108 self.nodes.iter().find(|(nid, _)| nid == &cycle_nodes[0])
109 {
110 write!(output, " {} ", CYCLE_ARROW).ok();
111 self.write_node(output, first_id, first_label);
112 }
113 }
114 }
115 }
116 writeln!(output).ok();
117 writeln!(output).ok();
118 writeln!(
119 output,
120 "This creates an infinite loop in error dependencies."
121 )
122 .ok();
123 } else {
124 writeln!(output, "Complex cycle detected in graph.").ok();
125 }
126 }
127
128 fn is_simple_chain(&self) -> bool {
130 if self.nodes.is_empty() {
131 return false;
132 }
133
134 let subgraphs = self.find_subgraphs();
136 if subgraphs.len() > 1 {
137 return false;
138 }
139
140 for &(node_id, _) in &self.nodes {
142 let parents = self.get_parents(node_id);
143 let children = self.get_children(node_id);
144
145 if parents.len() > 1 || children.len() > 1 {
146 return false;
147 }
148 }
149
150 true
151 }
152
153 fn render_horizontal(&self, output: &mut String) {
155 let roots: Vec<_> = self
157 .nodes
158 .iter()
159 .filter(|(id, _)| self.get_parents(*id).is_empty())
160 .collect();
161
162 if roots.is_empty() {
163 output.push_str("(no root)");
164 return;
165 }
166
167 let mut current_id = roots[0].0;
169 let mut visited = Vec::new();
170
171 loop {
172 visited.push(current_id);
173
174 if let Some(&(id, label)) = self.nodes.iter().find(|(nid, _)| *nid == current_id) {
176 self.write_node(output, id, label);
177 }
178
179 let children = self.get_children(current_id);
181
182 if children.is_empty() {
183 break;
184 }
185
186 write!(output, " {} ", ARROW_RIGHT).ok();
188
189 current_id = children[0];
191
192 if visited.contains(¤t_id) {
194 break;
195 }
196 }
197
198 writeln!(output).ok();
199 }
200
201 fn render_vertical(&self, output: &mut String) {
203 let subgraphs = self.find_subgraphs();
205
206 if subgraphs.len() > 1 {
207 for (i, subgraph_nodes) in subgraphs.iter().enumerate() {
209 if i > 0 {
210 writeln!(output).ok();
211 }
212 self.render_subgraph(output, subgraph_nodes);
213 }
214 return;
215 }
216
217 let level_data = self.calculate_levels();
219 let max_level = level_data.iter().map(|(_, l)| *l).max().unwrap_or(0);
220
221 let mut levels: Vec<Vec<usize>> = vec![Vec::new(); max_level + 1];
223 for (idx, level) in &level_data {
224 levels[*level].push(*idx);
225 }
226
227 self.reduce_crossings(&mut levels, max_level);
229
230 let node_x_coords = self.assign_x_coordinates(&mut levels, max_level);
232
233 let (level_widths, max_canvas_width) =
235 self.calculate_canvas_dimensions(&levels, &node_x_coords);
236
237 for (current_level, level_nodes) in levels.iter().enumerate() {
239 if level_nodes.is_empty() {
240 continue;
241 }
242
243 let level_width = level_widths[current_level];
245 let level_offset = if max_canvas_width > level_width {
246 (max_canvas_width - level_width) / 2
247 } else {
248 0
249 };
250
251 let min_x = level_nodes
253 .iter()
254 .map(|&idx| node_x_coords[idx])
255 .min()
256 .unwrap_or(0);
257
258 let mut current_col = 0;
260 for &idx in level_nodes {
261 let node_x = node_x_coords[idx] - min_x + level_offset;
262
263 while current_col < node_x {
265 output.push(' ');
266 current_col += 1;
267 }
268
269 let (id, label) = self.nodes[idx];
270 self.write_node(output, id, label);
272 current_col += self.get_node_width(idx); }
274 writeln!(output).ok();
275
276 if current_level < max_level {
278 let next_level_width = level_widths[current_level + 1];
279 let next_level_offset = if max_canvas_width > next_level_width {
280 (max_canvas_width - next_level_width) / 2
281 } else {
282 0
283 };
284
285 self.draw_connections_sugiyama(
286 output,
287 level_nodes,
288 &levels[current_level + 1],
289 &node_x_coords,
290 min_x,
291 level_offset,
292 next_level_offset,
293 );
294 }
295 }
296 }
297
298 fn draw_connections_sugiyama(
300 &self,
301 output: &mut String,
302 current_nodes: &[usize],
303 next_nodes: &[usize],
304 x_coords: &[usize],
305 current_min_x: usize,
306 current_offset: usize,
307 next_offset: usize,
308 ) {
309 if current_nodes.is_empty() || next_nodes.is_empty() {
310 return;
311 }
312
313 let current_centers: Vec<(usize, usize)> = current_nodes
315 .iter()
316 .map(|&idx| {
317 let width = self.get_node_width(idx);
318 let center = x_coords[idx] - current_min_x + current_offset + width / 2;
319 (idx, center)
320 })
321 .collect();
322
323 let next_min_x = next_nodes
324 .iter()
325 .map(|&idx| x_coords[idx])
326 .min()
327 .unwrap_or(0);
328 let next_centers: Vec<(usize, usize)> = next_nodes
329 .iter()
330 .map(|&idx| {
331 let width = self.get_node_width(idx);
332 let center = x_coords[idx] - next_min_x + next_offset + width / 2;
333 (idx, center)
334 })
335 .collect();
336
337 let mut connections: Vec<(usize, usize)> = Vec::new();
339 for &(curr_idx, from_pos) in ¤t_centers {
340 let node_id = self.nodes[curr_idx].0;
341 for child_id in self.get_children(node_id) {
342 if let Some(&(_, to_pos)) = next_centers
343 .iter()
344 .find(|(idx, _)| self.nodes[*idx].0 == child_id)
345 {
346 connections.push((from_pos, to_pos));
347 }
348 }
349 }
350
351 if connections.is_empty() {
352 return;
353 }
354
355 let mut target_groups: Vec<(usize, Vec<usize>)> = Vec::new();
357 for &(from, to) in &connections {
358 match target_groups.binary_search_by_key(&to, |(k, _)| *k) {
359 Ok(idx) => target_groups[idx].1.push(from),
360 Err(idx) => target_groups.insert(idx, (to, vec![from])),
361 }
362 }
363
364 let mut source_groups: Vec<(usize, Vec<usize>)> = Vec::new();
365 for &(from, to) in &connections {
366 match source_groups.binary_search_by_key(&from, |(k, _)| *k) {
367 Ok(idx) => source_groups[idx].1.push(to),
368 Err(idx) => source_groups.insert(idx, (from, vec![to])),
369 }
370 }
371
372 let has_convergence = target_groups.iter().any(|(_, v)| v.len() > 1);
373 let has_divergence = source_groups.iter().any(|(_, v)| v.len() > 1);
374
375 let min_pos = 0;
377 let max_pos = connections
378 .iter()
379 .flat_map(|(f, t)| [*f, *t])
380 .max()
381 .unwrap_or(0);
382
383 if has_convergence && !has_divergence {
385 self.draw_convergence_manhattan(output, &target_groups, min_pos, max_pos);
386 } else if has_divergence && !has_convergence {
387 self.draw_divergence_manhattan(output, &source_groups, min_pos, max_pos);
388 } else {
389 self.draw_simple_manhattan(output, &connections, min_pos, max_pos);
390 }
391 }
392
393 fn draw_convergence_manhattan(
394 &self,
395 output: &mut String,
396 target_groups: &[(usize, Vec<usize>)],
397 min_pos: usize,
398 max_pos: usize,
399 ) {
400 let all_sources: Vec<usize> = target_groups
401 .iter()
402 .flat_map(|(_, sources)| sources.iter().copied())
403 .collect();
404
405 for i in min_pos..=max_pos {
407 output.push(if all_sources.contains(&i) {
408 V_LINE
409 } else {
410 ' '
411 });
412 }
413 writeln!(output).ok();
414
415 for i in min_pos..=max_pos {
417 let mut ch = ' ';
418 for (_, sources) in target_groups.iter() {
419 if sources.len() <= 1 {
420 continue;
421 }
422 let min_src = *sources.iter().min().unwrap();
423 let max_src = *sources.iter().max().unwrap();
424 if i == min_src {
425 ch = CORNER_DR;
426 } else if i == max_src {
427 ch = CORNER_DL;
428 } else if sources.contains(&i) {
429 ch = TEE_UP;
430 } else if i > min_src && i < max_src {
431 ch = H_LINE;
432 }
433 }
434 output.push(ch);
435 }
436 writeln!(output).ok();
437
438 for i in min_pos..=max_pos {
440 output.push(if target_groups.iter().any(|(t, _)| *t == i) {
441 ARROW_DOWN
442 } else {
443 ' '
444 });
445 }
446 writeln!(output).ok();
447 }
448
449 fn draw_divergence_manhattan(
450 &self,
451 output: &mut String,
452 source_groups: &[(usize, Vec<usize>)],
453 min_pos: usize,
454 max_pos: usize,
455 ) {
456 let all_sources: Vec<usize> = source_groups.iter().map(|(s, _)| *s).collect();
457
458 for i in min_pos..=max_pos {
460 output.push(if all_sources.contains(&i) {
461 V_LINE
462 } else {
463 ' '
464 });
465 }
466 writeln!(output).ok();
467
468 for i in min_pos..=max_pos {
470 let mut ch = ' ';
471 for (_, targets) in source_groups.iter() {
472 if targets.len() <= 1 {
473 continue;
474 }
475 let min_tgt = *targets.iter().min().unwrap();
476 let max_tgt = *targets.iter().max().unwrap();
477 if i == min_tgt {
478 ch = CORNER_UR;
479 } else if i == max_tgt {
480 ch = CORNER_UL;
481 } else if targets.contains(&i) {
482 ch = TEE_DOWN;
483 } else if i > min_tgt && i < max_tgt {
484 ch = H_LINE;
485 }
486 }
487 output.push(ch);
488 }
489 writeln!(output).ok();
490
491 let all_targets: Vec<usize> = source_groups
493 .iter()
494 .flat_map(|(_, t)| t.iter().copied())
495 .collect();
496 for i in min_pos..=max_pos {
497 output.push(if all_targets.contains(&i) {
498 ARROW_DOWN
499 } else {
500 ' '
501 });
502 }
503 writeln!(output).ok();
504 }
505
506 fn draw_simple_manhattan(
507 &self,
508 output: &mut String,
509 connections: &[(usize, usize)],
510 min_pos: usize,
511 max_pos: usize,
512 ) {
513 for i in min_pos..=max_pos {
515 output.push(if connections.iter().any(|(f, _)| *f == i) {
516 V_LINE
517 } else {
518 ' '
519 });
520 }
521 writeln!(output).ok();
522
523 for i in min_pos..=max_pos {
525 output.push(if connections.iter().any(|(f, _)| *f == i) {
526 ARROW_DOWN
527 } else {
528 ' '
529 });
530 }
531 writeln!(output).ok();
532 }
533
534 pub(crate) fn render_subgraph(&self, output: &mut String, subgraph_indices: &[usize]) {
536 let _subgraph_node_ids: Vec<usize> = subgraph_indices
538 .iter()
539 .map(|&idx| self.nodes[idx].0)
540 .collect();
541
542 let level_data = self.calculate_levels_for_subgraph(subgraph_indices);
544 let max_level = level_data.iter().map(|(_, l)| *l).max().unwrap_or(0);
545
546 let mut levels: Vec<Vec<usize>> = vec![Vec::new(); max_level + 1];
548 for (idx, level) in level_data {
549 levels[level].push(idx);
550 }
551
552 if self.is_subgraph_simple_chain(subgraph_indices) {
554 let roots: Vec<_> = subgraph_indices
556 .iter()
557 .filter(|&&idx| {
558 let node_id = self.nodes[idx].0;
559 self.get_parents(node_id).is_empty()
560 })
561 .collect();
562
563 if let Some(&&root_idx) = roots.first() {
564 let mut current_id = self.nodes[root_idx].0;
565 let mut visited = Vec::new();
566
567 loop {
568 visited.push(current_id);
569
570 if let Some(&(id, label)) =
571 self.nodes.iter().find(|(nid, _)| *nid == current_id)
572 {
573 self.write_node(output, id, label);
574 }
575
576 let children = self.get_children(current_id);
577
578 if children.is_empty() {
579 break;
580 }
581
582 write!(output, " {} ", ARROW_RIGHT).ok();
583 current_id = children[0];
584
585 if visited.contains(¤t_id) {
586 break;
587 }
588 }
589
590 writeln!(output).ok();
591 }
592 return;
593 }
594
595 for (current_level, node_indices) in levels.iter().enumerate() {
597 if node_indices.is_empty() {
598 continue;
599 }
600
601 for (pos, &idx) in node_indices.iter().enumerate() {
603 let (id, label) = self.nodes[idx];
604 self.write_node(output, id, label);
605
606 if pos < node_indices.len() - 1 {
607 output.push_str(" ");
608 }
609 }
610 writeln!(output).ok();
611
612 if current_level < max_level {
614 self.draw_vertical_connections(output, node_indices, &levels[current_level + 1]);
615 }
616 }
617 }
618
619 fn draw_vertical_connections(
620 &self,
621 output: &mut String,
622 current_nodes: &[usize],
623 next_nodes: &[usize],
624 ) {
625 if current_nodes.is_empty() || next_nodes.is_empty() {
626 return;
627 }
628
629 let mut current_positions = Vec::new();
631 let mut pos = 0;
632 for &idx in current_nodes {
633 let label_len = self.get_node_width(idx);
634 let center = pos + label_len / 2;
635 current_positions.push((idx, center, pos, pos + label_len));
636 pos += label_len + 3; }
638
639 let mut next_positions = Vec::new();
641 let mut pos = 0;
642 for &idx in next_nodes {
643 let label_len = self.get_node_width(idx);
644 let center = pos + label_len / 2;
645 next_positions.push((idx, center));
646 pos += label_len + 3; }
648
649 let mut connections: Vec<(usize, usize, usize)> = Vec::new(); for &(current_idx, from_pos, _, _) in ¤t_positions {
653 let node_id = self.nodes[current_idx].0;
654 let children = self.get_children(node_id);
655
656 for child_id in children {
657 if let Some(&(_, to_pos)) = next_positions
658 .iter()
659 .find(|(idx, _)| self.nodes[*idx].0 == child_id)
660 {
661 connections.push((current_idx, from_pos, to_pos));
662 }
663 }
664 }
665
666 if connections.is_empty() {
667 return;
668 }
669
670 let mut target_groups: Vec<(usize, Vec<(usize, usize, usize)>)> = Vec::new();
673
674 for &conn in &connections {
675 match target_groups.binary_search_by_key(&conn.2, |(k, _)| *k) {
677 Ok(idx) => target_groups[idx].1.push(conn),
678 Err(idx) => target_groups.insert(idx, (conn.2, vec![conn])),
679 }
680 }
681
682 let has_any_convergence = target_groups.iter().any(|(_, v)| v.len() > 1);
684
685 let mut source_groups: Vec<(usize, Vec<(usize, usize, usize)>)> = Vec::new();
687
688 for &conn in &connections {
689 match source_groups.binary_search_by_key(&conn.0, |(k, _)| *k) {
690 Ok(idx) => source_groups[idx].1.push(conn),
691 Err(idx) => source_groups.insert(idx, (conn.0, vec![conn])),
692 }
693 }
694
695 let has_any_divergence = source_groups.iter().any(|(_, v)| v.len() > 1);
697
698 if has_any_convergence && !has_any_divergence {
700 self.draw_multiple_convergences(output, &target_groups);
702 } else if has_any_divergence && !has_any_convergence {
703 self.draw_multiple_divergences(output, &source_groups);
705 } else if has_any_convergence && has_any_divergence {
706 self.draw_simple_verticals(output, &connections);
708 } else {
709 self.draw_simple_verticals(output, &connections);
711 }
712 }
713
714 fn draw_multiple_convergences(
715 &self,
716 output: &mut String,
717 target_groups: &[(usize, Vec<(usize, usize, usize)>)],
718 ) {
719 let all_connections: Vec<_> = target_groups
721 .iter()
722 .flat_map(|(_, v)| v.iter().copied())
723 .collect();
724 let min_pos = all_connections
725 .iter()
726 .map(|(_, from, to)| (*from).min(*to))
727 .min()
728 .unwrap_or(0);
729 let max_pos = all_connections
730 .iter()
731 .map(|(_, from, to)| (*from).max(*to))
732 .max()
733 .unwrap_or(0);
734
735 for i in min_pos..=max_pos {
737 if all_connections.iter().any(|(_, from, _)| *from == i) {
738 output.push(V_LINE);
739 } else {
740 output.push(' ');
741 }
742 }
743 writeln!(output).ok();
744
745 for i in min_pos..=max_pos {
747 let mut char_at_pos = ' ';
748
749 for (_, conns) in target_groups.iter() {
750 if conns.len() <= 1 {
751 continue;
752 }
753
754 let sources: Vec<_> = conns.iter().map(|(_, from, _)| from).collect();
755 let min_source = **sources.iter().min().unwrap();
756 let max_source = **sources.iter().max().unwrap();
757
758 if i == min_source {
759 char_at_pos = CORNER_DR; } else if i == max_source {
761 char_at_pos = CORNER_DL; } else if sources.contains(&&i) {
763 char_at_pos = TEE_UP; } else if i > min_source && i < max_source {
765 if char_at_pos == ' ' {
766 char_at_pos = H_LINE; }
768 }
769 }
770
771 output.push(char_at_pos);
772 }
773 writeln!(output).ok();
774
775 for i in min_pos..=max_pos {
777 if target_groups.iter().any(|(target_pos, _)| *target_pos == i) {
778 output.push(ARROW_DOWN);
779 } else {
780 output.push(' ');
781 }
782 }
783 writeln!(output).ok();
784 }
785
786 fn draw_multiple_divergences(
787 &self,
788 output: &mut String,
789 source_groups: &[(usize, Vec<(usize, usize, usize)>)],
790 ) {
791 let all_connections: Vec<_> = source_groups
792 .iter()
793 .flat_map(|(_, v)| v.iter().copied())
794 .collect();
795 let min_pos = all_connections
796 .iter()
797 .map(|(_, from, to)| (*from).min(*to))
798 .min()
799 .unwrap_or(0);
800 let max_pos = all_connections
801 .iter()
802 .map(|(_, from, to)| (*from).max(*to))
803 .max()
804 .unwrap_or(0);
805
806 for i in 0..=max_pos {
808 if i < min_pos {
809 output.push(' ');
810 } else if all_connections.iter().any(|(_, from, _)| *from == i) {
811 output.push(V_LINE);
812 } else {
813 output.push(' ');
814 }
815 }
816 writeln!(output).ok();
817
818 for i in 0..=max_pos {
820 let mut char_at_pos = ' ';
821
822 if i >= min_pos {
823 for (_, conns) in source_groups.iter() {
824 if conns.len() <= 1 {
825 continue;
826 }
827
828 let targets: Vec<_> = conns.iter().map(|(_, _, to)| to).collect();
829 let min_target = **targets.iter().min().unwrap();
830 let max_target = **targets.iter().max().unwrap();
831
832 if i == min_target {
833 char_at_pos = CORNER_UR; } else if i == max_target {
835 char_at_pos = CORNER_UL; } else if targets.contains(&&i) {
837 char_at_pos = TEE_DOWN; } else if i > min_target && i < max_target {
839 if char_at_pos == ' ' {
840 char_at_pos = H_LINE; }
842 }
843 }
844 }
845
846 output.push(char_at_pos);
847 }
848 writeln!(output).ok();
849
850 for i in 0..=max_pos {
852 if i < min_pos {
853 output.push(' ');
854 } else if all_connections.iter().any(|(_, _, to)| *to == i) {
855 output.push(ARROW_DOWN);
856 } else {
857 output.push(' ');
858 }
859 }
860 writeln!(output).ok();
861 }
862
863 fn draw_simple_verticals(&self, output: &mut String, connections: &[(usize, usize, usize)]) {
864 let max_pos = connections
865 .iter()
866 .map(|(_, from, to)| (*from).max(*to))
867 .max()
868 .unwrap_or(0);
869
870 for i in 0..=max_pos {
872 if connections.iter().any(|(_, from, _)| *from == i) {
873 output.push(V_LINE);
874 } else {
875 output.push(' ');
876 }
877 }
878 writeln!(output).ok();
879
880 for i in 0..=max_pos {
882 if connections.iter().any(|(_, from, _)| *from == i) {
883 output.push(ARROW_DOWN);
884 } else {
885 output.push(' ');
886 }
887 }
888 writeln!(output).ok();
889 }
890}