1use crate::cfg::{BlockKind, Cfg, EdgeType, Terminator};
4use serde::{Deserialize, Serialize};
5use std::fmt::Write;
6
7pub fn export_dot(cfg: &Cfg) -> String {
9 export_dot_with_coords(cfg)
10}
11
12pub fn export_dot_with_coords(cfg: &Cfg) -> String {
19 let mut dot = String::from("digraph CFG {\n");
20 dot.push_str(" rankdir=TB;\n");
21 dot.push_str(" node [shape=box, style=rounded];\n\n");
22
23 let (max_coord_x, max_coord_y, _max_coord_z) = calculate_coordinate_ranges(cfg);
25
26 for node_idx in cfg.node_indices() {
28 if let Some(block) = cfg.node_weight(node_idx) {
29 let label = escape_dot_string(&format!(
30 "Block {}\\n{}\\n{}\\nCoords: X={}, Y={}, Z={}",
31 block.id,
32 format_block_kind(&block.kind),
33 format_terminator(&block.terminator),
34 block.coord_x,
35 block.coord_y,
36 block.coord_z
37 ));
38
39 let depth_color = get_depth_color(block.coord_x, max_coord_x);
41
42 let border_style = get_loop_border_style(block.coord_y, max_coord_y);
44
45 let base_style = match block.kind {
47 BlockKind::Entry => "fillcolor=lightgreen, style=filled",
48 BlockKind::Exit => "fillcolor=lightcoral, style=filled",
49 BlockKind::Normal => "",
50 };
51
52 writeln!(
53 dot,
54 " \"{}\" [label=\"{}\" {} {} {}];",
55 node_idx.index(),
56 label,
57 base_style,
58 depth_color,
59 border_style
60 )
61 .ok();
62 }
63 }
64
65 dot.push_str("\n");
67 for edge_idx in cfg.edge_indices() {
68 let (from, to) = cfg.edge_endpoints(edge_idx).unwrap();
69 if let Some(edge_type) = cfg.edge_weight(edge_idx) {
70 let color = edge_type.dot_color();
71 let label = edge_type.dot_label();
72 let label_attr = if label.is_empty() {
73 String::new()
74 } else {
75 format!(", label=\"{}\"", label)
76 };
77
78 writeln!(
79 dot,
80 " \"{}\" -> \"{}\" [color={}, style={}{}];",
81 from.index(),
82 to.index(),
83 color,
84 if *edge_type == EdgeType::Fallthrough {
85 "dashed"
86 } else {
87 "solid"
88 },
89 label_attr
90 )
91 .ok();
92 }
93 }
94
95 dot.push_str("}\n");
96 dot
97}
98
99fn calculate_coordinate_ranges(cfg: &Cfg) -> (i64, i64, i64) {
101 let mut max_x = 0;
102 let mut max_y = 0;
103 let mut max_z = 0;
104
105 for node_idx in cfg.node_indices() {
106 if let Some(block) = cfg.node_weight(node_idx) {
107 max_x = max_x.max(block.coord_x);
108 max_y = max_y.max(block.coord_y);
109 max_z = max_z.max(block.coord_z);
110 }
111 }
112
113 (max_x, max_y, max_z)
114}
115
116fn get_depth_color(coord_x: i64, max_x: i64) -> String {
118 if max_x == 0 {
119 return String::new();
120 }
121
122 let ratio = coord_x as f64 / max_x as f64;
124 let intensity = (ratio * 255.0) as u8;
125
126 let r = 200u8.saturating_sub(intensity / 2);
128 let g = 220u8.saturating_sub(intensity / 2);
129 let b = 255u8.saturating_sub(intensity / 4);
130
131 format!("fillcolor=\"#{:02x}{:02x}{:02x}\", style=filled", r, g, b)
132}
133
134fn get_loop_border_style(coord_y: i64, _max_y: i64) -> String {
136 if coord_y == 0 {
137 return String::new(); }
139
140 let width = 1 + coord_y.min(4); format!("penwidth={}, color=red", width)
143}
144
145pub fn export_human_with_coords(cfg: &Cfg, function_name: &str) -> String {
152 let mut output = String::new();
153
154 writeln!(output, "Control Flow Graph: {}", function_name).ok();
156 writeln!(output, "{}", "=".repeat(60)).ok();
157
158 let stats = calculate_coordinate_statistics(cfg);
160
161 writeln!(output, "\n4D Coordinate Statistics:").ok();
163 writeln!(
164 output,
165 " Dominator Depth (X): max={}, avg={:.1}",
166 stats.max_coord_x, stats.avg_coord_x
167 )
168 .ok();
169 writeln!(
170 output,
171 " Loop Nesting (Y): max={}, avg={:.1}",
172 stats.max_coord_y, stats.avg_coord_y
173 )
174 .ok();
175 writeln!(
176 output,
177 " Branch Distance (Z): max={}, avg={:.1}",
178 stats.max_coord_z, stats.avg_coord_z
179 )
180 .ok();
181
182 writeln!(output, "\nBlocks ({} total):", cfg.node_count()).ok();
184 for node_idx in cfg.node_indices() {
185 if let Some(block) = cfg.node_weight(node_idx) {
186 writeln!(
187 output,
188 "\n Block {} [{}]:",
189 block.id,
190 format_block_kind(&block.kind)
191 )
192 .ok();
193 writeln!(
194 output,
195 " Terminator: {}",
196 format_terminator(&block.terminator)
197 )
198 .ok();
199 writeln!(
200 output,
201 " Coordinates: X={}, Y={}, Z={}",
202 block.coord_x, block.coord_y, block.coord_z
203 )
204 .ok();
205
206 if block.coord_x > 0 {
208 writeln!(
209 output,
210 " → {} levels deep in control flow",
211 block.coord_x
212 )
213 .ok();
214 }
215 if block.coord_y > 0 {
216 writeln!(output, " → Inside {} loop(s)", block.coord_y).ok();
217 }
218 if block.coord_z > 1 {
219 writeln!(
220 output,
221 " → {} conditional branches from entry",
222 block.coord_z
223 )
224 .ok();
225 }
226 }
227 }
228
229 output
230}
231
232#[derive(Debug, Clone)]
234pub struct CoordinateStatistics {
235 pub max_coord_x: i64,
236 pub max_coord_y: i64,
237 pub max_coord_z: i64,
238 pub avg_coord_x: f64,
239 pub avg_coord_y: f64,
240 pub avg_coord_z: f64,
241 pub total_blocks: usize,
242}
243
244pub fn calculate_coordinate_statistics(cfg: &Cfg) -> CoordinateStatistics {
246 let mut sum_x = 0i64;
247 let mut sum_y = 0i64;
248 let mut sum_z = 0i64;
249 let mut max_x = 0i64;
250 let mut max_y = 0i64;
251 let mut max_z = 0i64;
252 let mut count = 0usize;
253
254 for node_idx in cfg.node_indices() {
255 if let Some(block) = cfg.node_weight(node_idx) {
256 sum_x += block.coord_x;
257 sum_y += block.coord_y;
258 sum_z += block.coord_z;
259 max_x = max_x.max(block.coord_x);
260 max_y = max_y.max(block.coord_y);
261 max_z = max_z.max(block.coord_z);
262 count += 1;
263 }
264 }
265
266 let avg_x = if count > 0 {
267 sum_x as f64 / count as f64
268 } else {
269 0.0
270 };
271 let avg_y = if count > 0 {
272 sum_y as f64 / count as f64
273 } else {
274 0.0
275 };
276 let avg_z = if count > 0 {
277 sum_z as f64 / count as f64
278 } else {
279 0.0
280 };
281
282 CoordinateStatistics {
283 max_coord_x: max_x,
284 max_coord_y: max_y,
285 max_coord_z: max_z,
286 avg_coord_x: avg_x,
287 avg_coord_y: avg_y,
288 avg_coord_z: avg_z,
289 total_blocks: count,
290 }
291}
292
293fn escape_dot_string(s: &str) -> String {
294 s.replace('"', "\\\"")
295}
296
297fn format_block_kind(kind: &BlockKind) -> &'static str {
298 match kind {
299 BlockKind::Entry => "ENTRY",
300 BlockKind::Normal => "NORMAL",
301 BlockKind::Exit => "EXIT",
302 }
303}
304
305fn format_terminator(term: &Terminator) -> String {
306 match term {
307 Terminator::Goto { target } => format!("goto {}", target),
308 Terminator::SwitchInt { targets, otherwise } => {
309 format!("switch({} targets, otherwise {})", targets.len(), otherwise)
310 }
311 Terminator::Return => "return".to_string(),
312 Terminator::Unreachable => "unreachable".to_string(),
313 Terminator::Call { target, unwind } => {
314 format!("call {:?}, unwind {:?}", target, unwind)
315 }
316 Terminator::Abort(msg) => format!("abort({})", msg),
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct CFGExport {
323 pub function_name: String,
324 pub entry: Option<usize>,
325 pub exits: Vec<usize>,
326 pub blocks: Vec<BlockExport>,
327 pub edges: Vec<EdgeExport>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct BlockCoverage {
333 pub hit_count: i64,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct BlockExport {
339 pub id: usize,
340 pub kind: String,
341 pub statements: Vec<String>,
342 pub terminator: String,
343 pub source_location: Option<String>,
344 pub coord_x: i64,
347 pub coord_y: i64,
349 pub coord_z: i64,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub coverage: Option<BlockCoverage>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct EdgeExport {
358 pub from: usize,
359 pub to: usize,
360 pub kind: String,
361}
362
363pub fn export_json(
369 cfg: &Cfg,
370 function_name: &str,
371 coverage: Option<&std::collections::HashMap<i64, i64>>,
372) -> CFGExport {
373 use crate::cfg::analysis;
374
375 let entry = analysis::find_entry(cfg).map(|idx| idx.index());
376 let exits = analysis::find_exits(cfg)
377 .iter()
378 .map(|idx| idx.index())
379 .collect();
380
381 let blocks: Vec<_> = cfg
382 .node_indices()
383 .map(|idx| {
384 let block = cfg.node_weight(idx).unwrap();
385 let block_coverage = coverage.and_then(|cov_map| {
386 block
387 .db_id
388 .and_then(|db_id| cov_map.get(&db_id))
389 .map(|&hit_count| BlockCoverage { hit_count })
390 });
391 BlockExport {
392 id: block.id,
393 kind: format_block_kind(&block.kind).to_string(),
394 statements: block.statements.clone(),
395 terminator: format_terminator(&block.terminator),
396 source_location: block.source_location.as_ref().map(|loc| loc.display()),
397 coord_x: block.coord_x,
398 coord_y: block.coord_y,
399 coord_z: block.coord_z,
400 coverage: block_coverage,
401 }
402 })
403 .collect();
404
405 let edges: Vec<_> = cfg
406 .edge_indices()
407 .map(|idx| {
408 let (from, to) = cfg.edge_endpoints(idx).unwrap();
409 let edge_type = cfg.edge_weight(idx).unwrap();
410 EdgeExport {
411 from: from.index(),
412 to: to.index(),
413 kind: format!("{:?}", edge_type),
414 }
415 })
416 .collect();
417
418 CFGExport {
419 function_name: function_name.to_string(),
420 entry,
421 exits,
422 blocks,
423 edges,
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use crate::cfg::BasicBlock;
431 use petgraph::graph::DiGraph;
432
433 fn create_test_cfg() -> Cfg {
434 let mut g = DiGraph::new();
435
436 let b0 = g.add_node(BasicBlock {
437 id: 0,
438 db_id: None,
439 kind: BlockKind::Entry,
440 statements: vec!["x = 1".to_string()],
441 terminator: Terminator::Goto { target: 1 },
442 source_location: None,
443 coord_x: 0,
444 coord_y: 0,
445 coord_z: 0,
446 });
447
448 let b1 = g.add_node(BasicBlock {
449 id: 1,
450 db_id: None,
451 kind: BlockKind::Normal,
452 statements: vec!["if x > 0".to_string()],
453 terminator: Terminator::SwitchInt {
454 targets: vec![2],
455 otherwise: 3,
456 },
457 source_location: None,
458 coord_x: 1,
459 coord_y: 0,
460 coord_z: 1,
461 });
462
463 let b2 = g.add_node(BasicBlock {
464 id: 2,
465 db_id: None,
466 kind: BlockKind::Exit,
467 statements: vec!["return true".to_string()],
468 terminator: Terminator::Return,
469 source_location: None,
470 coord_x: 2,
471 coord_y: 0,
472 coord_z: 2,
473 });
474
475 let b3 = g.add_node(BasicBlock {
476 id: 3,
477 db_id: None,
478 kind: BlockKind::Exit,
479 statements: vec!["return false".to_string()],
480 terminator: Terminator::Return,
481 source_location: None,
482 coord_x: 2,
483 coord_y: 0,
484 coord_z: 3,
485 });
486
487 g.add_edge(b0, b1, EdgeType::Fallthrough);
488 g.add_edge(b1, b2, EdgeType::TrueBranch);
489 g.add_edge(b1, b3, EdgeType::FalseBranch);
490
491 g
492 }
493
494 #[test]
495 fn test_export_dot() {
496 let cfg = create_test_cfg();
497 let dot = export_dot(&cfg);
498
499 assert!(dot.contains("digraph CFG"));
500 assert!(dot.contains("Block 0"));
501 assert!(dot.contains("ENTRY"));
502 assert!(dot.contains("color=green")); assert!(dot.contains("color=red")); }
505
506 #[test]
507 fn test_export_json() {
508 let cfg = create_test_cfg();
509 let export = export_json(&cfg, "test_function", None);
510
511 assert_eq!(export.function_name, "test_function");
512 assert_eq!(export.entry, Some(0));
513 assert_eq!(export.exits.len(), 2); assert_eq!(export.blocks.len(), 4);
515 assert_eq!(export.edges.len(), 3);
516
517 assert_eq!(export.blocks[0].kind, "ENTRY");
519 assert_eq!(export.blocks[2].kind, "EXIT");
520
521 assert!(export.edges.iter().any(|e| e.kind == "TrueBranch"));
523 assert!(export.edges.iter().any(|e| e.kind == "FalseBranch"));
524 }
525
526 #[test]
527 fn test_dot_is_valid_graphviz() {
528 let cfg = create_test_cfg();
529 let dot = export_dot(&cfg);
530
531 assert!(dot.starts_with("digraph CFG {"));
533 assert!(dot.ends_with("}\n"));
534
535 let first_edge_pos = dot.find("->").unwrap();
539 let section_separator = dot.find("\n\n").unwrap();
540 assert!(
541 section_separator < first_edge_pos,
542 "Node section should end before edges start"
543 );
544
545 assert!(dot.contains("rankdir=TB;"));
547 assert!(dot.contains("node [shape=box"));
548 }
549
550 #[test]
551 fn test_export_dot_with_coords_includes_coordinates() {
552 let mut cfg = create_test_cfg();
554
555 for node_idx in cfg.node_indices() {
557 if let Some(block) = cfg.node_weight_mut(node_idx) {
558 block.coord_x = node_idx.index() as i64;
559 block.coord_y = if node_idx.index() > 1 { 1 } else { 0 };
560 block.coord_z = node_idx.index() as i64 / 2;
561 }
562 }
563
564 let dot = export_dot_with_coords(&cfg);
566
567 assert!(
569 dot.contains("Coords:"),
570 "DOT should include coordinate labels"
571 );
572 assert!(dot.contains("X="), "DOT should include X coordinate");
573 assert!(dot.contains("Y="), "DOT should include Y coordinate");
574 assert!(dot.contains("Z="), "DOT should include Z coordinate");
575 }
576
577 #[test]
578 fn test_export_human_with_coords_includes_statistics() {
579 let mut cfg = create_test_cfg();
581
582 for node_idx in cfg.node_indices() {
584 if let Some(block) = cfg.node_weight_mut(node_idx) {
585 block.coord_x = node_idx.index() as i64;
586 block.coord_y = (node_idx.index() / 2) as i64;
587 block.coord_z = node_idx.index() as i64;
588 }
589 }
590
591 let output = export_human_with_coords(&cfg, "test_function");
593
594 assert!(output.contains("Control Flow Graph: test_function"));
596 assert!(output.contains("4D Coordinate Statistics"));
597 assert!(output.contains("Dominator Depth (X)"));
598 assert!(output.contains("Loop Nesting (Y)"));
599 assert!(output.contains("Branch Distance (Z)"));
600 assert!(output.contains("max="));
601 assert!(output.contains("avg="));
602 }
603
604 #[test]
605 fn test_calculate_coordinate_statistics() {
606 let mut cfg = create_test_cfg();
608
609 for (i, node_idx) in cfg.node_indices().enumerate() {
611 if let Some(block) = cfg.node_weight_mut(node_idx) {
612 block.coord_x = i as i64;
613 block.coord_y = (i / 2) as i64;
614 block.coord_z = (i * 2) as i64;
615 }
616 }
617
618 let stats = calculate_coordinate_statistics(&cfg);
620
621 assert_eq!(stats.total_blocks, 4);
623 assert_eq!(stats.max_coord_x, 3);
624 assert_eq!(stats.max_coord_y, 1);
625 assert_eq!(stats.max_coord_z, 6);
626
627 assert!((stats.avg_coord_x - 1.5).abs() < 0.01);
629 }
630
631 #[test]
632 fn test_coordinate_statistics_empty_cfg() {
633 let cfg: Cfg = petgraph::graph::DiGraph::new();
635
636 let stats = calculate_coordinate_statistics(&cfg);
638
639 assert_eq!(stats.total_blocks, 0);
641 assert_eq!(stats.max_coord_x, 0);
642 assert_eq!(stats.max_coord_y, 0);
643 assert_eq!(stats.max_coord_z, 0);
644 assert_eq!(stats.avg_coord_x, 0.0);
645 assert_eq!(stats.avg_coord_y, 0.0);
646 assert_eq!(stats.avg_coord_z, 0.0);
647 }
648
649 #[test]
650 fn test_export_json_includes_coordinates() {
651 let mut cfg = create_test_cfg();
653
654 for node_idx in cfg.node_indices() {
656 if let Some(block) = cfg.node_weight_mut(node_idx) {
657 block.coord_x = 5;
658 block.coord_y = 2;
659 block.coord_z = 3;
660 }
661 }
662
663 let export = export_json(&cfg, "test_function", None);
665
666 assert!(!export.blocks.is_empty());
668 for block in &export.blocks {
669 assert_eq!(block.coord_x, 5, "Block should have coord_x set");
670 assert_eq!(block.coord_y, 2, "Block should have coord_y set");
671 assert_eq!(block.coord_z, 3, "Block should have coord_z set");
672 }
673 }
674
675 #[test]
676 fn test_export_human_spatial_insights() {
677 let mut cfg = create_test_cfg();
679
680 if let Some(block) = cfg.node_weight_mut(petgraph::graph::NodeIndex::new(2)) {
682 block.coord_x = 4; block.coord_y = 2; block.coord_z = 5; }
686
687 let output = export_human_with_coords(&cfg, "complex_function");
689
690 assert!(output.contains("4 levels deep in control flow"));
692 assert!(output.contains("Inside 2 loop(s)"));
693 assert!(output.contains("5 conditional branches from entry"));
694 }
695
696 #[test]
697 fn test_get_depth_color_gradient() {
698 let color_0 = get_depth_color(0, 10);
700 let color_5 = get_depth_color(5, 10);
701 let color_10 = get_depth_color(10, 10);
702
703 assert!(color_0.contains("fillcolor"));
705 assert!(color_5.contains("fillcolor"));
706 assert!(color_10.contains("fillcolor"));
707
708 assert_ne!(color_0, color_10);
710 }
711
712 #[test]
713 fn test_get_loop_border_style() {
714 let style_0 = get_loop_border_style(0, 5);
716 let style_1 = get_loop_border_style(1, 5);
717 let style_3 = get_loop_border_style(3, 5);
718
719 assert!(style_0.is_empty());
721
722 assert!(style_1.contains("red"));
724 assert!(style_3.contains("red"));
725
726 assert!(style_1.contains("penwidth=2"));
728 assert!(style_3.contains("penwidth=4"));
729 }
730}