use crate::cfg::{BlockKind, Cfg, EdgeType, Terminator};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
pub fn export_dot(cfg: &Cfg) -> String {
export_dot_with_coords(cfg)
}
pub fn export_dot_with_coords(cfg: &Cfg) -> String {
let mut dot = String::from("digraph CFG {\n");
dot.push_str(" rankdir=TB;\n");
dot.push_str(" node [shape=box, style=rounded];\n\n");
let (max_coord_x, max_coord_y, _max_coord_z) = calculate_coordinate_ranges(cfg);
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight(node_idx) {
let label = escape_dot_string(&format!(
"Block {}\\n{}\\n{}\\nCoords: X={}, Y={}, Z={}",
block.id,
format_block_kind(&block.kind),
format_terminator(&block.terminator),
block.coord_x,
block.coord_y,
block.coord_z
));
let depth_color = get_depth_color(block.coord_x, max_coord_x);
let border_style = get_loop_border_style(block.coord_y, max_coord_y);
let base_style = match block.kind {
BlockKind::Entry => "fillcolor=lightgreen, style=filled",
BlockKind::Exit => "fillcolor=lightcoral, style=filled",
BlockKind::Normal => "",
};
writeln!(
dot,
" \"{}\" [label=\"{}\" {} {} {}];",
node_idx.index(),
label,
base_style,
depth_color,
border_style
)
.ok();
}
}
dot.push_str("\n");
for edge_idx in cfg.edge_indices() {
let (from, to) = cfg.edge_endpoints(edge_idx).unwrap();
if let Some(edge_type) = cfg.edge_weight(edge_idx) {
let color = edge_type.dot_color();
let label = edge_type.dot_label();
let label_attr = if label.is_empty() {
String::new()
} else {
format!(", label=\"{}\"", label)
};
writeln!(
dot,
" \"{}\" -> \"{}\" [color={}, style={}{}];",
from.index(),
to.index(),
color,
if *edge_type == EdgeType::Fallthrough {
"dashed"
} else {
"solid"
},
label_attr
)
.ok();
}
}
dot.push_str("}\n");
dot
}
fn calculate_coordinate_ranges(cfg: &Cfg) -> (i64, i64, i64) {
let mut max_x = 0;
let mut max_y = 0;
let mut max_z = 0;
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight(node_idx) {
max_x = max_x.max(block.coord_x);
max_y = max_y.max(block.coord_y);
max_z = max_z.max(block.coord_z);
}
}
(max_x, max_y, max_z)
}
fn get_depth_color(coord_x: i64, max_x: i64) -> String {
if max_x == 0 {
return String::new();
}
let ratio = coord_x as f64 / max_x as f64;
let intensity = (ratio * 255.0) as u8;
let r = 200u8.saturating_sub(intensity / 2);
let g = 220u8.saturating_sub(intensity / 2);
let b = 255u8.saturating_sub(intensity / 4);
format!("fillcolor=\"#{:02x}{:02x}{:02x}\", style=filled", r, g, b)
}
fn get_loop_border_style(coord_y: i64, _max_y: i64) -> String {
if coord_y == 0 {
return String::new(); }
let width = 1 + coord_y.min(4); format!("penwidth={}, color=red", width)
}
pub fn export_human_with_coords(cfg: &Cfg, function_name: &str) -> String {
let mut output = String::new();
writeln!(output, "Control Flow Graph: {}", function_name).ok();
writeln!(output, "{}", "=".repeat(60)).ok();
let stats = calculate_coordinate_statistics(cfg);
writeln!(output, "\n4D Coordinate Statistics:").ok();
writeln!(
output,
" Dominator Depth (X): max={}, avg={:.1}",
stats.max_coord_x, stats.avg_coord_x
)
.ok();
writeln!(
output,
" Loop Nesting (Y): max={}, avg={:.1}",
stats.max_coord_y, stats.avg_coord_y
)
.ok();
writeln!(
output,
" Branch Distance (Z): max={}, avg={:.1}",
stats.max_coord_z, stats.avg_coord_z
)
.ok();
writeln!(output, "\nBlocks ({} total):", cfg.node_count()).ok();
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight(node_idx) {
writeln!(
output,
"\n Block {} [{}]:",
block.id,
format_block_kind(&block.kind)
)
.ok();
writeln!(
output,
" Terminator: {}",
format_terminator(&block.terminator)
)
.ok();
writeln!(
output,
" Coordinates: X={}, Y={}, Z={}",
block.coord_x, block.coord_y, block.coord_z
)
.ok();
if block.coord_x > 0 {
writeln!(
output,
" → {} levels deep in control flow",
block.coord_x
)
.ok();
}
if block.coord_y > 0 {
writeln!(output, " → Inside {} loop(s)", block.coord_y).ok();
}
if block.coord_z > 1 {
writeln!(
output,
" → {} conditional branches from entry",
block.coord_z
)
.ok();
}
}
}
output
}
#[derive(Debug, Clone)]
pub struct CoordinateStatistics {
pub max_coord_x: i64,
pub max_coord_y: i64,
pub max_coord_z: i64,
pub avg_coord_x: f64,
pub avg_coord_y: f64,
pub avg_coord_z: f64,
pub total_blocks: usize,
}
pub fn calculate_coordinate_statistics(cfg: &Cfg) -> CoordinateStatistics {
let mut sum_x = 0i64;
let mut sum_y = 0i64;
let mut sum_z = 0i64;
let mut max_x = 0i64;
let mut max_y = 0i64;
let mut max_z = 0i64;
let mut count = 0usize;
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight(node_idx) {
sum_x += block.coord_x;
sum_y += block.coord_y;
sum_z += block.coord_z;
max_x = max_x.max(block.coord_x);
max_y = max_y.max(block.coord_y);
max_z = max_z.max(block.coord_z);
count += 1;
}
}
let avg_x = if count > 0 {
sum_x as f64 / count as f64
} else {
0.0
};
let avg_y = if count > 0 {
sum_y as f64 / count as f64
} else {
0.0
};
let avg_z = if count > 0 {
sum_z as f64 / count as f64
} else {
0.0
};
CoordinateStatistics {
max_coord_x: max_x,
max_coord_y: max_y,
max_coord_z: max_z,
avg_coord_x: avg_x,
avg_coord_y: avg_y,
avg_coord_z: avg_z,
total_blocks: count,
}
}
fn escape_dot_string(s: &str) -> String {
s.replace('"', "\\\"")
}
fn format_block_kind(kind: &BlockKind) -> &'static str {
match kind {
BlockKind::Entry => "ENTRY",
BlockKind::Normal => "NORMAL",
BlockKind::Exit => "EXIT",
}
}
fn format_terminator(term: &Terminator) -> String {
match term {
Terminator::Goto { target } => format!("goto {}", target),
Terminator::SwitchInt { targets, otherwise } => {
format!("switch({} targets, otherwise {})", targets.len(), otherwise)
}
Terminator::Return => "return".to_string(),
Terminator::Unreachable => "unreachable".to_string(),
Terminator::Call { target, unwind } => {
format!("call {:?}, unwind {:?}", target, unwind)
}
Terminator::Abort(msg) => format!("abort({})", msg),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CFGExport {
pub function_name: String,
pub entry: Option<usize>,
pub exits: Vec<usize>,
pub blocks: Vec<BlockExport>,
pub edges: Vec<EdgeExport>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockCoverage {
pub hit_count: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockExport {
pub id: usize,
pub kind: String,
pub statements: Vec<String>,
pub terminator: String,
pub source_location: Option<String>,
pub coord_x: i64,
pub coord_y: i64,
pub coord_z: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub coverage: Option<BlockCoverage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeExport {
pub from: usize,
pub to: usize,
pub kind: String,
}
pub fn export_json(
cfg: &Cfg,
function_name: &str,
coverage: Option<&std::collections::HashMap<i64, i64>>,
) -> CFGExport {
use crate::cfg::analysis;
let entry = analysis::find_entry(cfg).map(|idx| idx.index());
let exits = analysis::find_exits(cfg)
.iter()
.map(|idx| idx.index())
.collect();
let blocks: Vec<_> = cfg
.node_indices()
.map(|idx| {
let block = cfg.node_weight(idx).unwrap();
let block_coverage = coverage.and_then(|cov_map| {
block
.db_id
.and_then(|db_id| cov_map.get(&db_id))
.map(|&hit_count| BlockCoverage { hit_count })
});
BlockExport {
id: block.id,
kind: format_block_kind(&block.kind).to_string(),
statements: block.statements.clone(),
terminator: format_terminator(&block.terminator),
source_location: block.source_location.as_ref().map(|loc| loc.display()),
coord_x: block.coord_x,
coord_y: block.coord_y,
coord_z: block.coord_z,
coverage: block_coverage,
}
})
.collect();
let edges: Vec<_> = cfg
.edge_indices()
.map(|idx| {
let (from, to) = cfg.edge_endpoints(idx).unwrap();
let edge_type = cfg.edge_weight(idx).unwrap();
EdgeExport {
from: from.index(),
to: to.index(),
kind: format!("{:?}", edge_type),
}
})
.collect();
CFGExport {
function_name: function_name.to_string(),
entry,
exits,
blocks,
edges,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::BasicBlock;
use petgraph::graph::DiGraph;
fn create_test_cfg() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["if x > 0".to_string()],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 1,
coord_y: 0,
coord_z: 1,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return true".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 2,
coord_y: 0,
coord_z: 2,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return false".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 2,
coord_y: 0,
coord_z: 3,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g
}
#[test]
fn test_export_dot() {
let cfg = create_test_cfg();
let dot = export_dot(&cfg);
assert!(dot.contains("digraph CFG"));
assert!(dot.contains("Block 0"));
assert!(dot.contains("ENTRY"));
assert!(dot.contains("color=green")); assert!(dot.contains("color=red")); }
#[test]
fn test_export_json() {
let cfg = create_test_cfg();
let export = export_json(&cfg, "test_function", None);
assert_eq!(export.function_name, "test_function");
assert_eq!(export.entry, Some(0));
assert_eq!(export.exits.len(), 2); assert_eq!(export.blocks.len(), 4);
assert_eq!(export.edges.len(), 3);
assert_eq!(export.blocks[0].kind, "ENTRY");
assert_eq!(export.blocks[2].kind, "EXIT");
assert!(export.edges.iter().any(|e| e.kind == "TrueBranch"));
assert!(export.edges.iter().any(|e| e.kind == "FalseBranch"));
}
#[test]
fn test_dot_is_valid_graphviz() {
let cfg = create_test_cfg();
let dot = export_dot(&cfg);
assert!(dot.starts_with("digraph CFG {"));
assert!(dot.ends_with("}\n"));
let first_edge_pos = dot.find("->").unwrap();
let section_separator = dot.find("\n\n").unwrap();
assert!(
section_separator < first_edge_pos,
"Node section should end before edges start"
);
assert!(dot.contains("rankdir=TB;"));
assert!(dot.contains("node [shape=box"));
}
#[test]
fn test_export_dot_with_coords_includes_coordinates() {
let mut cfg = create_test_cfg();
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight_mut(node_idx) {
block.coord_x = node_idx.index() as i64;
block.coord_y = if node_idx.index() > 1 { 1 } else { 0 };
block.coord_z = node_idx.index() as i64 / 2;
}
}
let dot = export_dot_with_coords(&cfg);
assert!(
dot.contains("Coords:"),
"DOT should include coordinate labels"
);
assert!(dot.contains("X="), "DOT should include X coordinate");
assert!(dot.contains("Y="), "DOT should include Y coordinate");
assert!(dot.contains("Z="), "DOT should include Z coordinate");
}
#[test]
fn test_export_human_with_coords_includes_statistics() {
let mut cfg = create_test_cfg();
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight_mut(node_idx) {
block.coord_x = node_idx.index() as i64;
block.coord_y = (node_idx.index() / 2) as i64;
block.coord_z = node_idx.index() as i64;
}
}
let output = export_human_with_coords(&cfg, "test_function");
assert!(output.contains("Control Flow Graph: test_function"));
assert!(output.contains("4D Coordinate Statistics"));
assert!(output.contains("Dominator Depth (X)"));
assert!(output.contains("Loop Nesting (Y)"));
assert!(output.contains("Branch Distance (Z)"));
assert!(output.contains("max="));
assert!(output.contains("avg="));
}
#[test]
fn test_calculate_coordinate_statistics() {
let mut cfg = create_test_cfg();
for (i, node_idx) in cfg.node_indices().enumerate() {
if let Some(block) = cfg.node_weight_mut(node_idx) {
block.coord_x = i as i64;
block.coord_y = (i / 2) as i64;
block.coord_z = (i * 2) as i64;
}
}
let stats = calculate_coordinate_statistics(&cfg);
assert_eq!(stats.total_blocks, 4);
assert_eq!(stats.max_coord_x, 3);
assert_eq!(stats.max_coord_y, 1);
assert_eq!(stats.max_coord_z, 6);
assert!((stats.avg_coord_x - 1.5).abs() < 0.01);
}
#[test]
fn test_coordinate_statistics_empty_cfg() {
let cfg: Cfg = petgraph::graph::DiGraph::new();
let stats = calculate_coordinate_statistics(&cfg);
assert_eq!(stats.total_blocks, 0);
assert_eq!(stats.max_coord_x, 0);
assert_eq!(stats.max_coord_y, 0);
assert_eq!(stats.max_coord_z, 0);
assert_eq!(stats.avg_coord_x, 0.0);
assert_eq!(stats.avg_coord_y, 0.0);
assert_eq!(stats.avg_coord_z, 0.0);
}
#[test]
fn test_export_json_includes_coordinates() {
let mut cfg = create_test_cfg();
for node_idx in cfg.node_indices() {
if let Some(block) = cfg.node_weight_mut(node_idx) {
block.coord_x = 5;
block.coord_y = 2;
block.coord_z = 3;
}
}
let export = export_json(&cfg, "test_function", None);
assert!(!export.blocks.is_empty());
for block in &export.blocks {
assert_eq!(block.coord_x, 5, "Block should have coord_x set");
assert_eq!(block.coord_y, 2, "Block should have coord_y set");
assert_eq!(block.coord_z, 3, "Block should have coord_z set");
}
}
#[test]
fn test_export_human_spatial_insights() {
let mut cfg = create_test_cfg();
if let Some(block) = cfg.node_weight_mut(petgraph::graph::NodeIndex::new(2)) {
block.coord_x = 4; block.coord_y = 2; block.coord_z = 5; }
let output = export_human_with_coords(&cfg, "complex_function");
assert!(output.contains("4 levels deep in control flow"));
assert!(output.contains("Inside 2 loop(s)"));
assert!(output.contains("5 conditional branches from entry"));
}
#[test]
fn test_get_depth_color_gradient() {
let color_0 = get_depth_color(0, 10);
let color_5 = get_depth_color(5, 10);
let color_10 = get_depth_color(10, 10);
assert!(color_0.contains("fillcolor"));
assert!(color_5.contains("fillcolor"));
assert!(color_10.contains("fillcolor"));
assert_ne!(color_0, color_10);
}
#[test]
fn test_get_loop_border_style() {
let style_0 = get_loop_border_style(0, 5);
let style_1 = get_loop_border_style(1, 5);
let style_3 = get_loop_border_style(3, 5);
assert!(style_0.is_empty());
assert!(style_1.contains("red"));
assert!(style_3.contains("red"));
assert!(style_1.contains("penwidth=2"));
assert!(style_3.contains("penwidth=4"));
}
}