Skip to main content

mirage/cfg/
export.rs

1//! CFG export to DOT and JSON formats with 4D coordinate visualization
2
3use crate::cfg::{BlockKind, Cfg, EdgeType, Terminator};
4use serde::{Deserialize, Serialize};
5use std::fmt::Write;
6
7/// Export CFG to DOT format for Graphviz with 4D coordinate visualization
8pub fn export_dot(cfg: &Cfg) -> String {
9    export_dot_with_coords(cfg)
10}
11
12/// Export CFG to DOT format with 4D coordinate-based coloring
13///
14/// This function creates a GraphViz DOT visualization where:
15/// - Node colors represent dominator depth (coord_x)
16/// - Node borders represent loop nesting (coord_y)
17/// - Node labels include all 4D coordinates
18pub 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    // Calculate coordinate ranges for color mapping
24    let (max_coord_x, max_coord_y, _max_coord_z) = calculate_coordinate_ranges(cfg);
25
26    // Define nodes with coordinate-based styling
27    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            // Color based on dominator depth (coord_x) - deeper = darker
40            let depth_color = get_depth_color(block.coord_x, max_coord_x);
41
42            // Border style based on loop nesting (coord_y)
43            let border_style = get_loop_border_style(block.coord_y, max_coord_y);
44
45            // Base style for block kind
46            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    // Define edges
66    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
99/// Calculate maximum coordinate values for color mapping
100fn 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
116/// Get color based on dominator depth (coord_x)
117fn get_depth_color(coord_x: i64, max_x: i64) -> String {
118    if max_x == 0 {
119        return String::new();
120    }
121
122    // Map coord_x to a color gradient (light to dark blue)
123    let ratio = coord_x as f64 / max_x as f64;
124    let intensity = (ratio * 255.0) as u8;
125
126    // Light blue to dark blue gradient
127    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
134/// Get border style based on loop nesting (coord_y)
135fn get_loop_border_style(coord_y: i64, _max_y: i64) -> String {
136    if coord_y == 0 {
137        return String::new(); // No border for non-loop nodes
138    }
139
140    // Thicker borders for deeper nesting
141    let width = 1 + coord_y.min(4); // Cap at width 5
142    format!("penwidth={}, color=red", width)
143}
144
145/// Export CFG to human-readable format with 4D coordinate statistics
146///
147/// This function creates a detailed human-readable representation that includes:
148/// - Block information with coordinates
149/// - Coordinate statistics (max, average, etc.)
150/// - Spatial analysis insights
151pub fn export_human_with_coords(cfg: &Cfg, function_name: &str) -> String {
152    let mut output = String::new();
153
154    // Header with function name
155    writeln!(output, "Control Flow Graph: {}", function_name).ok();
156    writeln!(output, "{}", "=".repeat(60)).ok();
157
158    // Calculate coordinate statistics
159    let stats = calculate_coordinate_statistics(cfg);
160
161    // Display statistics
162    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    // Display blocks with coordinates
183    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            // Add spatial insights
207            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/// Coordinate statistics for a CFG
233#[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
244/// Calculate comprehensive coordinate statistics for a CFG
245pub 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/// Complete CFG export for JSON serialization
321#[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/// Coverage data for a single block
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct BlockCoverage {
333    /// Number of times this block was executed
334    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    /// 4D Spatial Coordinates
345    /// X coordinate: dominator depth (control flow hierarchy level)
346    pub coord_x: i64,
347    /// Y coordinate: loop nesting depth (how many loops surround this block)
348    pub coord_y: i64,
349    /// Z coordinate: branch distance from entry point
350    pub coord_z: i64,
351    /// Coverage data (only present when coverage is available)
352    #[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
363/// Export CFG to JSON format
364///
365/// Optionally includes per-block coverage data keyed by the original database block ID
366/// (`cfg_blocks.id`). If `coverage` is `Some`, each `BlockExport` will include a
367/// `coverage` field when its `db_id` matches an entry in the map.
368pub 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")); // TrueBranch
503        assert!(dot.contains("color=red")); // FalseBranch
504    }
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); // blocks 2 and 3
514        assert_eq!(export.blocks.len(), 4);
515        assert_eq!(export.edges.len(), 3);
516
517        // Check block kinds
518        assert_eq!(export.blocks[0].kind, "ENTRY");
519        assert_eq!(export.blocks[2].kind, "EXIT");
520
521        // Check edge types
522        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        // Basic validation: starts correctly, ends correctly
532        assert!(dot.starts_with("digraph CFG {"));
533        assert!(dot.ends_with("}\n"));
534
535        // Check that edges section starts after newline following nodes
536        // Edges start with "  \"" followed by number and " ->"
537        // Nodes end with "];" before the "\n\n" separator
538        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        // Verify basic DOT structure elements
546        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        // Given: A CFG with coordinate data
553        let mut cfg = create_test_cfg();
554
555        // Set some coordinates for testing
556        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        // When: Exporting to DOT with coordinates
565        let dot = export_dot_with_coords(&cfg);
566
567        // Then: DOT output should include coordinate information
568        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        // Given: A CFG with coordinate data
580        let mut cfg = create_test_cfg();
581
582        // Set specific coordinates for testing
583        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        // When: Exporting to human-readable format with coordinates
592        let output = export_human_with_coords(&cfg, "test_function");
593
594        // Then: Output should include coordinate statistics
595        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        // Given: A CFG with varying coordinate values
607        let mut cfg = create_test_cfg();
608
609        // Set specific coordinates
610        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        // When: Calculating statistics
619        let stats = calculate_coordinate_statistics(&cfg);
620
621        // Then: Statistics should be calculated correctly
622        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        // Check averages (should be (0+1+2+3)/4 = 1.5 for X)
628        assert!((stats.avg_coord_x - 1.5).abs() < 0.01);
629    }
630
631    #[test]
632    fn test_coordinate_statistics_empty_cfg() {
633        // Given: An empty CFG
634        let cfg: Cfg = petgraph::graph::DiGraph::new();
635
636        // When: Calculating statistics
637        let stats = calculate_coordinate_statistics(&cfg);
638
639        // Then: Should return zero statistics
640        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        // Given: A CFG with coordinate data
652        let mut cfg = create_test_cfg();
653
654        // Set specific coordinates
655        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        // When: Exporting to JSON
664        let export = export_json(&cfg, "test_function", None);
665
666        // Then: JSON export should include coordinate fields
667        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        // Given: A CFG with complex coordinate data
678        let mut cfg = create_test_cfg();
679
680        // Create a block with deep nesting
681        if let Some(block) = cfg.node_weight_mut(petgraph::graph::NodeIndex::new(2)) {
682            block.coord_x = 4; // Deep dominator depth
683            block.coord_y = 2; // Nested in 2 loops
684            block.coord_z = 5; // Far from entry
685        }
686
687        // When: Exporting to human-readable format
688        let output = export_human_with_coords(&cfg, "complex_function");
689
690        // Then: Output should include spatial insights
691        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        // Test color gradient generation for different depths
699        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        // Check that colors are generated
704        assert!(color_0.contains("fillcolor"));
705        assert!(color_5.contains("fillcolor"));
706        assert!(color_10.contains("fillcolor"));
707
708        // Check that deeper depths produce different colors (darker)
709        assert_ne!(color_0, color_10);
710    }
711
712    #[test]
713    fn test_get_loop_border_style() {
714        // Test border styles for different nesting levels
715        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        // No border for non-loop nodes
720        assert!(style_0.is_empty());
721
722        // Red borders for loop nodes
723        assert!(style_1.contains("red"));
724        assert!(style_3.contains("red"));
725
726        // Different widths for different nesting levels
727        assert!(style_1.contains("penwidth=2"));
728        assert!(style_3.contains("penwidth=4"));
729    }
730}