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}