Skip to main content

decy_debugger/
visualize_ast.rs

1//! C AST visualization for debugging
2//!
3//! Provides formatted visualization of C ASTs parsed by clang-sys.
4//! Inspired by spydecy's Python AST visualizer.
5
6use anyhow::{Context, Result};
7use colored::Colorize;
8use decy_parser::{CParser, Expression, Function, Statement};
9use std::fs;
10use std::path::Path;
11
12/// Visualize C source as AST
13///
14/// # Errors
15///
16/// Returns an error if the file cannot be read or parsed
17pub fn visualize_c_ast(file_path: &Path, use_colors: bool) -> Result<String> {
18    // Read the source file
19    let source = fs::read_to_string(file_path)
20        .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
21
22    // Parse to AST
23    let parser = CParser::new()?;
24    let ast = parser.parse(&source).context("Failed to parse C source")?;
25    let functions = ast.functions();
26
27    // Format the output
28    let mut output = String::new();
29
30    // Header
31    if use_colors {
32        output.push_str(&format!(
33            "{}",
34            "╔══════════════════════════════════════════════════════════╗\n".cyan()
35        ));
36        output.push_str(&format!(
37            "{}",
38            "║  Decy Debugger: C AST Visualization                     ║\n".cyan()
39        ));
40        output.push_str(&format!(
41            "{}",
42            "╚══════════════════════════════════════════════════════════╝\n".cyan()
43        ));
44    } else {
45        output.push_str("╔══════════════════════════════════════════════════════════╗\n");
46        output.push_str("║  Decy Debugger: C AST Visualization                     ║\n");
47        output.push_str("╚══════════════════════════════════════════════════════════╝\n");
48    }
49    output.push('\n');
50
51    // File info
52    let file_label = if use_colors {
53        "File:".bold().to_string()
54    } else {
55        "File:".to_string()
56    };
57    output.push_str(&format!("{} {}\n", file_label, file_path.display()));
58
59    let size_label = if use_colors {
60        "Size:".bold().to_string()
61    } else {
62        "Size:".to_string()
63    };
64    output.push_str(&format!(
65        "{} {} lines\n",
66        size_label,
67        source.lines().count()
68    ));
69    output.push('\n');
70
71    // Source code preview
72    let source_header = if use_colors {
73        "═══ Source Code ═══".yellow().bold().to_string()
74    } else {
75        "═══ Source Code ═══".to_string()
76    };
77    output.push_str(&format!("{source_header}\n"));
78
79    for (i, line) in source.lines().enumerate() {
80        if use_colors {
81            output.push_str(&format!("{:3} │ {}\n", (i + 1).to_string().dimmed(), line));
82        } else {
83            output.push_str(&format!("{:3} │ {}\n", i + 1, line));
84        }
85    }
86    output.push('\n');
87
88    // AST tree
89    let ast_header = if use_colors {
90        "═══ Abstract Syntax Tree ═══".green().bold().to_string()
91    } else {
92        "═══ Abstract Syntax Tree ═══".to_string()
93    };
94    output.push_str(&format!("{}\n", ast_header));
95
96    for function in functions {
97        format_function(function, 0, &mut output, use_colors);
98    }
99    output.push('\n');
100
101    // Statistics
102    let stats_header = if use_colors {
103        "═══ Statistics ═══".blue().bold().to_string()
104    } else {
105        "═══ Statistics ═══".to_string()
106    };
107    output.push_str(&format!("{}\n", stats_header));
108
109    let func_count_label = if use_colors {
110        "Functions:".bold().to_string()
111    } else {
112        "Functions:".to_string()
113    };
114    output.push_str(&format!("  {} {}\n", func_count_label, functions.len()));
115
116    let total_statements: usize = functions.iter().map(|f| f.body.len()).sum();
117    let stmt_count_label = if use_colors {
118        "Total statements:".bold().to_string()
119    } else {
120        "Total statements:".to_string()
121    };
122    output.push_str(&format!("  {} {}\n", stmt_count_label, total_statements));
123
124    Ok(output)
125}
126
127/// Format a function node with indentation
128fn format_function(function: &Function, depth: usize, output: &mut String, use_colors: bool) {
129    let indent = "  ".repeat(depth);
130
131    let node_type = if use_colors {
132        format!("Function: {}", function.name)
133            .green()
134            .bold()
135            .to_string()
136    } else {
137        format!("Function: {}", function.name)
138    };
139
140    output.push_str(&format!("{}├─ {}", indent, node_type));
141
142    // Return type
143    let return_type_str = format!("{:?}", function.return_type);
144    if use_colors {
145        output.push_str(&format!(" → {}", return_type_str.dimmed()));
146    } else {
147        output.push_str(&format!(" → {}", return_type_str));
148    }
149    output.push('\n');
150
151    // Parameters
152    if !function.parameters.is_empty() {
153        let params_label = if use_colors {
154            "Parameters:".blue().to_string()
155        } else {
156            "Parameters:".to_string()
157        };
158        output.push_str(&format!("{}  {} ", indent, params_label));
159
160        for (i, param) in function.parameters.iter().enumerate() {
161            if i > 0 {
162                output.push_str(", ");
163            }
164            output.push_str(&format!("{}: {:?}", param.name, param.param_type));
165        }
166        output.push('\n');
167    }
168
169    // Body statements
170    if !function.body.is_empty() {
171        let body_label = if use_colors {
172            "Body:".cyan().to_string()
173        } else {
174            "Body:".to_string()
175        };
176        output.push_str(&format!("{}  {}\n", indent, body_label));
177
178        for stmt in &function.body {
179            format_statement(stmt, depth + 2, output, use_colors);
180        }
181    }
182}
183
184/// Format a statement node
185fn format_statement(stmt: &Statement, depth: usize, output: &mut String, use_colors: bool) {
186    let indent = "  ".repeat(depth);
187
188    let stmt_str = match stmt {
189        Statement::Return(Some(expr)) => {
190            let label = if use_colors {
191                "Return".red().to_string()
192            } else {
193                "Return".to_string()
194            };
195            format!("{}: {}", label, format_expression(expr, use_colors))
196        }
197        Statement::Return(None) => {
198            if use_colors {
199                "Return (void)".red().to_string()
200            } else {
201                "Return (void)".to_string()
202            }
203        }
204        Statement::Assignment { target, value } => {
205            let label = if use_colors {
206                "Assignment".yellow().to_string()
207            } else {
208                "Assignment".to_string()
209            };
210            format!(
211                "{}: {} = {}",
212                label,
213                target,
214                format_expression(value, use_colors)
215            )
216        }
217        Statement::If {
218            condition,
219            then_block,
220            else_block,
221        } => {
222            let mut s = if use_colors {
223                format!(
224                    "{}: {}",
225                    "If".magenta(),
226                    format_expression(condition, use_colors)
227                )
228            } else {
229                format!("If: {}", format_expression(condition, use_colors))
230            };
231
232            s.push_str(&format!(" (then: {} stmts", then_block.len()));
233            if let Some(else_b) = else_block {
234                s.push_str(&format!(", else: {} stmts)", else_b.len()));
235            } else {
236                s.push(')');
237            }
238            s
239        }
240        Statement::While { condition, body } => {
241            let label = if use_colors {
242                "While".magenta().to_string()
243            } else {
244                "While".to_string()
245            };
246            format!(
247                "{}: {} ({} stmts)",
248                label,
249                format_expression(condition, use_colors),
250                body.len()
251            )
252        }
253        Statement::For {
254            init,
255            condition,
256            increment,
257            body,
258        } => {
259            let label = if use_colors {
260                "For".magenta().to_string()
261            } else {
262                "For".to_string()
263            };
264            format!(
265                "{}: init={:?}, cond={:?}, inc={:?} ({} stmts)",
266                label,
267                init,
268                condition,
269                increment,
270                body.len()
271            )
272        }
273        Statement::VariableDeclaration {
274            name,
275            var_type,
276            initializer,
277        } => {
278            let label = if use_colors {
279                "VarDecl".cyan().to_string()
280            } else {
281                "VarDecl".to_string()
282            };
283            let mut s = format!("{}: {:?} {}", label, var_type, name);
284            if let Some(init) = initializer {
285                s.push_str(&format!(" = {}", format_expression(init, use_colors)));
286            }
287            s
288        }
289        _ => format!("{:?}", stmt),
290    };
291
292    output.push_str(&format!("{}├─ {}\n", indent, stmt_str));
293}
294
295/// Format an expression for display
296fn format_expression(expr: &Expression, use_colors: bool) -> String {
297    match expr {
298        Expression::IntLiteral(n) => {
299            if use_colors {
300                n.to_string().bright_yellow().to_string()
301            } else {
302                n.to_string()
303            }
304        }
305        Expression::Variable(name) => {
306            if use_colors {
307                name.blue().to_string()
308            } else {
309                name.clone()
310            }
311        }
312        Expression::BinaryOp { left, op, right } => {
313            format!(
314                "({} {:?} {})",
315                format_expression(left, use_colors),
316                op,
317                format_expression(right, use_colors)
318            )
319        }
320        Expression::FunctionCall {
321            function,
322            arguments,
323        } => {
324            let args_str = arguments
325                .iter()
326                .map(|arg| format_expression(arg, use_colors))
327                .collect::<Vec<_>>()
328                .join(", ");
329
330            if use_colors {
331                format!("{}({})", function.magenta(), args_str)
332            } else {
333                format!("{}({})", function, args_str)
334            }
335        }
336        _ => format!("{:?}", expr),
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use std::io::Write;
344    use tempfile::NamedTempFile;
345
346    #[test]
347    fn test_visualize_simple_function() {
348        let mut temp_file = NamedTempFile::new().unwrap();
349        writeln!(temp_file, "int add(int a, int b) {{ return a + b; }}").unwrap();
350
351        let result = visualize_c_ast(temp_file.path(), false);
352        assert!(result.is_ok());
353
354        let output = result.unwrap();
355        assert!(output.contains("Function: add"));
356        assert!(output.contains("Return"));
357    }
358
359    #[test]
360    fn test_visualize_with_colors() {
361        let mut temp_file = NamedTempFile::new().unwrap();
362        writeln!(temp_file, "int main() {{ return 0; }}").unwrap();
363
364        let result = visualize_c_ast(temp_file.path(), true);
365        assert!(result.is_ok());
366
367        // Just verify it produces output (color codes make exact matching hard)
368        let output = result.unwrap();
369        assert!(!output.is_empty());
370        assert!(output.contains("main"));
371    }
372
373    // ============================================================================
374    // Coverage: for loop, while loop, if/else, variable decl, assignment
375    // ============================================================================
376
377    #[test]
378    fn test_visualize_for_loop() {
379        let mut temp_file = NamedTempFile::new().unwrap();
380        writeln!(
381            temp_file,
382            "int main() {{ int i; for (i = 0; i < 10; i = i + 1) {{ }} return 0; }}"
383        )
384        .unwrap();
385
386        let result = visualize_c_ast(temp_file.path(), false);
387        assert!(result.is_ok());
388        let output = result.unwrap();
389        assert!(output.contains("Function: main"));
390    }
391
392    #[test]
393    fn test_visualize_while_loop() {
394        let mut temp_file = NamedTempFile::new().unwrap();
395        writeln!(
396            temp_file,
397            "int main() {{ int x = 10; while (x) {{ x = x - 1; }} return 0; }}"
398        )
399        .unwrap();
400
401        let result = visualize_c_ast(temp_file.path(), false);
402        assert!(result.is_ok());
403        let output = result.unwrap();
404        assert!(output.contains("Function: main"));
405    }
406
407    #[test]
408    fn test_visualize_if_else() {
409        let mut temp_file = NamedTempFile::new().unwrap();
410        writeln!(
411            temp_file,
412            "int main() {{ int x = 5; if (x) {{ return 1; }} else {{ return 0; }} }}"
413        )
414        .unwrap();
415
416        let result = visualize_c_ast(temp_file.path(), false);
417        assert!(result.is_ok());
418        let output = result.unwrap();
419        assert!(output.contains("Function: main"));
420    }
421
422    #[test]
423    fn test_visualize_variable_declaration() {
424        let mut temp_file = NamedTempFile::new().unwrap();
425        writeln!(
426            temp_file,
427            "int main() {{ int x = 42; int y = x + 1; return y; }}"
428        )
429        .unwrap();
430
431        let result = visualize_c_ast(temp_file.path(), false);
432        assert!(result.is_ok());
433        let output = result.unwrap();
434        assert!(output.contains("Function: main"));
435    }
436
437    #[test]
438    fn test_visualize_assignment() {
439        let mut temp_file = NamedTempFile::new().unwrap();
440        writeln!(
441            temp_file,
442            "int main() {{ int x = 0; x = 42; return x; }}"
443        )
444        .unwrap();
445
446        let result = visualize_c_ast(temp_file.path(), false);
447        assert!(result.is_ok());
448    }
449
450    #[test]
451    fn test_visualize_function_call() {
452        let mut temp_file = NamedTempFile::new().unwrap();
453        writeln!(
454            temp_file,
455            "int add(int a, int b) {{ return a + b; }}\nint main() {{ return add(2, 3); }}"
456        )
457        .unwrap();
458
459        let result = visualize_c_ast(temp_file.path(), false);
460        assert!(result.is_ok());
461        let output = result.unwrap();
462        // Should show both functions
463        assert!(output.contains("Function: add") || output.contains("Function: main"));
464    }
465
466    #[test]
467    fn test_visualize_multiple_parameters() {
468        let mut temp_file = NamedTempFile::new().unwrap();
469        writeln!(
470            temp_file,
471            "int add3(int a, int b, int c) {{ return a + b + c; }}"
472        )
473        .unwrap();
474
475        let result = visualize_c_ast(temp_file.path(), false);
476        assert!(result.is_ok());
477        let output = result.unwrap();
478        assert!(output.contains("Parameters:"));
479    }
480
481    #[test]
482    fn test_visualize_void_return() {
483        let mut temp_file = NamedTempFile::new().unwrap();
484        writeln!(temp_file, "void noop() {{ return; }}").unwrap();
485
486        let result = visualize_c_ast(temp_file.path(), false);
487        assert!(result.is_ok());
488    }
489
490    #[test]
491    fn test_visualize_statistics() {
492        let mut temp_file = NamedTempFile::new().unwrap();
493        writeln!(
494            temp_file,
495            "int f1() {{ return 1; }}\nint f2() {{ return 2; }}"
496        )
497        .unwrap();
498
499        let result = visualize_c_ast(temp_file.path(), false);
500        assert!(result.is_ok());
501        let output = result.unwrap();
502        assert!(output.contains("Statistics"));
503        assert!(output.contains("Functions:"));
504        assert!(output.contains("Total statements:"));
505    }
506
507    #[test]
508    fn test_visualize_colors_for_loop() {
509        let mut temp_file = NamedTempFile::new().unwrap();
510        writeln!(
511            temp_file,
512            "int main() {{ int i; for (i = 0; i < 5; i = i + 1) {{ }} return 0; }}"
513        )
514        .unwrap();
515
516        let result = visualize_c_ast(temp_file.path(), true);
517        assert!(result.is_ok());
518        let output = result.unwrap();
519        assert!(!output.is_empty());
520    }
521
522    #[test]
523    fn test_visualize_colors_if_else() {
524        let mut temp_file = NamedTempFile::new().unwrap();
525        writeln!(
526            temp_file,
527            "int main() {{ int x = 1; if (x) {{ return 1; }} else {{ return 0; }} }}"
528        )
529        .unwrap();
530
531        let result = visualize_c_ast(temp_file.path(), true);
532        assert!(result.is_ok());
533    }
534
535    // ============================================================================
536    // Coverage: error paths
537    // ============================================================================
538
539    #[test]
540    fn test_visualize_nonexistent_file() {
541        let result = visualize_c_ast(Path::new("/nonexistent/file.c"), false);
542        assert!(result.is_err());
543    }
544
545    // ============================================================================
546    // Coverage: format_expression and format_statement direct tests
547    // ============================================================================
548
549    #[test]
550    fn test_format_expression_int_literal_no_colors() {
551        let expr = Expression::IntLiteral(42);
552        let result = format_expression(&expr, false);
553        assert_eq!(result, "42");
554    }
555
556    #[test]
557    fn test_format_expression_int_literal_with_colors() {
558        let expr = Expression::IntLiteral(42);
559        let result = format_expression(&expr, true);
560        // Color codes wrap the number
561        assert!(result.contains("42"));
562    }
563
564    #[test]
565    fn test_format_expression_variable_no_colors() {
566        let expr = Expression::Variable("x".to_string());
567        let result = format_expression(&expr, false);
568        assert_eq!(result, "x");
569    }
570
571    #[test]
572    fn test_format_expression_variable_with_colors() {
573        let expr = Expression::Variable("x".to_string());
574        let result = format_expression(&expr, true);
575        assert!(result.contains("x"));
576    }
577
578    #[test]
579    fn test_format_expression_binary_op() {
580        let expr = Expression::BinaryOp {
581            left: Box::new(Expression::IntLiteral(1)),
582            op: decy_parser::parser::BinaryOperator::Add,
583            right: Box::new(Expression::IntLiteral(2)),
584        };
585        let result = format_expression(&expr, false);
586        assert!(result.contains("1"));
587        assert!(result.contains("2"));
588        assert!(result.contains("Add"));
589    }
590
591    #[test]
592    fn test_format_expression_function_call_no_colors() {
593        let expr = Expression::FunctionCall {
594            function: "printf".to_string(),
595            arguments: vec![Expression::Variable("msg".to_string())],
596        };
597        let result = format_expression(&expr, false);
598        assert!(result.contains("printf"));
599        assert!(result.contains("msg"));
600    }
601
602    #[test]
603    fn test_format_expression_function_call_with_colors() {
604        let expr = Expression::FunctionCall {
605            function: "printf".to_string(),
606            arguments: vec![Expression::IntLiteral(0)],
607        };
608        let result = format_expression(&expr, true);
609        assert!(result.contains("printf"));
610    }
611
612    #[test]
613    fn test_format_expression_catchall() {
614        let expr = Expression::StringLiteral("hello".to_string());
615        let result = format_expression(&expr, false);
616        // Falls through to Debug format
617        assert!(!result.is_empty());
618    }
619
620    #[test]
621    fn test_format_statement_return_some_no_colors() {
622        let stmt = Statement::Return(Some(Expression::IntLiteral(0)));
623        let mut output = String::new();
624        format_statement(&stmt, 0, &mut output, false);
625        assert!(output.contains("Return"));
626        assert!(output.contains("0"));
627    }
628
629    #[test]
630    fn test_format_statement_return_none_no_colors() {
631        let stmt = Statement::Return(None);
632        let mut output = String::new();
633        format_statement(&stmt, 0, &mut output, false);
634        assert!(output.contains("Return (void)"));
635    }
636
637    #[test]
638    fn test_format_statement_return_none_with_colors() {
639        let stmt = Statement::Return(None);
640        let mut output = String::new();
641        format_statement(&stmt, 0, &mut output, true);
642        assert!(output.contains("Return"));
643    }
644
645    #[test]
646    fn test_format_statement_assignment_no_colors() {
647        let stmt = Statement::Assignment {
648            target: "x".to_string(),
649            value: Expression::IntLiteral(42),
650        };
651        let mut output = String::new();
652        format_statement(&stmt, 0, &mut output, false);
653        assert!(output.contains("Assignment"));
654        assert!(output.contains("x"));
655    }
656
657    #[test]
658    fn test_format_statement_if_with_else_no_colors() {
659        let stmt = Statement::If {
660            condition: Expression::Variable("x".to_string()),
661            then_block: vec![Statement::Return(Some(Expression::IntLiteral(1)))],
662            else_block: Some(vec![Statement::Return(Some(Expression::IntLiteral(0)))]),
663        };
664        let mut output = String::new();
665        format_statement(&stmt, 0, &mut output, false);
666        assert!(output.contains("If"));
667        assert!(output.contains("then: 1 stmts"));
668        assert!(output.contains("else: 1 stmts"));
669    }
670
671    #[test]
672    fn test_format_statement_if_with_else_with_colors() {
673        let stmt = Statement::If {
674            condition: Expression::Variable("x".to_string()),
675            then_block: vec![Statement::Return(Some(Expression::IntLiteral(1)))],
676            else_block: Some(vec![Statement::Return(Some(Expression::IntLiteral(0)))]),
677        };
678        let mut output = String::new();
679        format_statement(&stmt, 0, &mut output, true);
680        assert!(output.contains("else: 1 stmts"));
681    }
682
683    #[test]
684    fn test_format_statement_while_no_colors() {
685        let stmt = Statement::While {
686            condition: Expression::Variable("running".to_string()),
687            body: vec![Statement::Return(Some(Expression::IntLiteral(0)))],
688        };
689        let mut output = String::new();
690        format_statement(&stmt, 0, &mut output, false);
691        assert!(output.contains("While"));
692        assert!(output.contains("1 stmts"));
693    }
694
695    #[test]
696    fn test_format_statement_for_no_colors() {
697        let stmt = Statement::For {
698            init: vec![Statement::VariableDeclaration {
699                name: "i".to_string(),
700                var_type: decy_parser::Type::Int,
701                initializer: Some(Expression::IntLiteral(0)),
702            }],
703            condition: Some(Expression::Variable("i".to_string())),
704            increment: vec![],
705            body: vec![],
706        };
707        let mut output = String::new();
708        format_statement(&stmt, 0, &mut output, false);
709        assert!(output.contains("For"));
710    }
711
712    #[test]
713    fn test_format_statement_var_decl_no_colors() {
714        let stmt = Statement::VariableDeclaration {
715            name: "x".to_string(),
716            var_type: decy_parser::Type::Int,
717            initializer: Some(Expression::IntLiteral(42)),
718        };
719        let mut output = String::new();
720        format_statement(&stmt, 0, &mut output, false);
721        assert!(output.contains("VarDecl"));
722        assert!(output.contains("x"));
723    }
724
725    #[test]
726    fn test_format_statement_var_decl_no_init() {
727        let stmt = Statement::VariableDeclaration {
728            name: "y".to_string(),
729            var_type: decy_parser::Type::Int,
730            initializer: None,
731        };
732        let mut output = String::new();
733        format_statement(&stmt, 0, &mut output, false);
734        assert!(output.contains("VarDecl"));
735        assert!(output.contains("y"));
736    }
737
738    #[test]
739    fn test_format_statement_catchall() {
740        let stmt = Statement::Break;
741        let mut output = String::new();
742        format_statement(&stmt, 0, &mut output, false);
743        assert!(!output.is_empty());
744    }
745
746    #[test]
747    fn test_format_function_no_body() {
748        let function = Function {
749            name: "empty".to_string(),
750            return_type: decy_parser::Type::Void,
751            parameters: vec![],
752            body: vec![],
753        };
754        let mut output = String::new();
755        format_function(&function, 0, &mut output, false);
756        assert!(output.contains("Function: empty"));
757    }
758
759    #[test]
760    fn test_format_function_with_colors() {
761        let function = Function {
762            name: "test_fn".to_string(),
763            return_type: decy_parser::Type::Int,
764            parameters: vec![decy_parser::Parameter::new(
765                "x".to_string(),
766                decy_parser::Type::Int,
767            )],
768            body: vec![Statement::Return(Some(Expression::Variable(
769                "x".to_string(),
770            )))],
771        };
772        let mut output = String::new();
773        format_function(&function, 0, &mut output, true);
774        assert!(output.contains("test_fn"));
775    }
776}