Skip to main content

spydecy_debugger/
visualize.rs

1//! AST visualization for debugging
2//!
3//! This module provides formatted visualization of ASTs for debugging purposes.
4
5use anyhow::{Context, Result};
6use colored::Colorize;
7use spydecy_c::{cpython, parser::CAST};
8use spydecy_python::parser::PythonAST;
9use std::fs;
10use std::path::Path;
11
12/// Visualize Python source as AST
13///
14/// # Errors
15///
16/// Returns an error if the file cannot be read or parsed
17pub fn visualize_python(file_path: &Path) -> 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 filename = file_path.to_string_lossy().to_string();
24    let ast = spydecy_python::parser::parse(&source, &filename)
25        .context("Failed to parse Python source")?;
26
27    // Format the output
28    let mut output = String::new();
29
30    // Header
31    output.push_str(&format!(
32        "{}",
33        "╔══════════════════════════════════════════════════════════╗\n".cyan()
34    ));
35    output.push_str(&format!(
36        "{}",
37        "║  Spydecy Debugger: Python AST Visualization             ║\n".cyan()
38    ));
39    output.push_str(&format!(
40        "{}",
41        "╚══════════════════════════════════════════════════════════╝\n".cyan()
42    ));
43    output.push('\n');
44
45    // File info
46    output.push_str(&format!("{} {}\n", "File:".bold(), file_path.display()));
47    output.push_str(&format!(
48        "{} {} lines\n",
49        "Size:".bold(),
50        source.lines().count()
51    ));
52    output.push('\n');
53
54    // Source code preview
55    output.push_str(&format!("{}\n", "═══ Source Code ═══".yellow().bold()));
56    for (i, line) in source.lines().enumerate() {
57        output.push_str(&format!("{:3} │ {}\n", (i + 1).to_string().dimmed(), line));
58    }
59    output.push('\n');
60
61    // AST tree
62    output.push_str(&format!(
63        "{}\n",
64        "═══ Abstract Syntax Tree ═══".green().bold()
65    ));
66    format_ast_node(&ast, 0, &mut output);
67    output.push('\n');
68
69    // Statistics
70    output.push_str(&format!("{}\n", "═══ Statistics ═══".blue().bold()));
71    let node_count = count_nodes(&ast);
72    output.push_str(&format!("  {} {}\n", "Total AST nodes:".bold(), node_count));
73    output.push_str(&format!(
74        "  {} {}\n",
75        "Root node type:".bold(),
76        ast.node_type
77    ));
78    if !ast.children.is_empty() {
79        output.push_str(&format!(
80            "  {} {}\n",
81            "Direct children:".bold(),
82            ast.children.len()
83        ));
84    }
85
86    Ok(output)
87}
88
89/// Format an AST node with indentation
90fn format_ast_node(node: &PythonAST, depth: usize, output: &mut String) {
91    let indent = "  ".repeat(depth);
92    let connector = if depth > 0 { "├─ " } else { "" };
93
94    // Node type (colored)
95    let node_type_colored = match node.node_type.as_str() {
96        "Module" => node.node_type.cyan().bold(),
97        "FunctionDef" => node.node_type.green().bold(),
98        "ClassDef" => node.node_type.yellow().bold(),
99        "Call" => node.node_type.magenta(),
100        "Return" => node.node_type.red(),
101        "Name" => node.node_type.blue(),
102        _ => node.node_type.white(),
103    };
104
105    output.push_str(&format!("{}{}{}", indent, connector, node_type_colored));
106
107    // Node attributes
108    if !node.attributes.is_empty() {
109        output.push_str(" (");
110        let mut first = true;
111        for (key, value) in &node.attributes {
112            if !first {
113                output.push_str(", ");
114            }
115            output.push_str(&format!("{}={}", key.dimmed(), value.bright_white()));
116            first = false;
117        }
118        output.push(')');
119    }
120
121    // Source location
122    if let Some(lineno) = node.lineno {
123        output.push_str(&format!(" {}", format!("@L{lineno}").dimmed()));
124    }
125
126    output.push('\n');
127
128    // Recursively format children
129    for child in &node.children {
130        format_ast_node(child, depth + 1, output);
131    }
132}
133
134/// Count total nodes in AST
135fn count_nodes(node: &PythonAST) -> usize {
136    1 + node.children.iter().map(count_nodes).sum::<usize>()
137}
138
139/// Visualize C source as AST with `CPython` API annotations
140///
141/// # Errors
142///
143/// Returns an error if the file cannot be read or parsed
144pub fn visualize_c(file_path: &Path) -> Result<String> {
145    // Read the source file
146    let source = fs::read_to_string(file_path)
147        .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
148
149    // Parse to AST
150    let filename = file_path.to_string_lossy().to_string();
151    let parser = spydecy_c::parser::CParser::new().context("Failed to create C parser")?;
152    let ast = parser
153        .parse(&source, &filename)
154        .context("Failed to parse C source")?;
155
156    // Format the output
157    let mut output = String::new();
158
159    // Header
160    output.push_str(&format!(
161        "{}",
162        "╔══════════════════════════════════════════════════════════╗\n".cyan()
163    ));
164    output.push_str(&format!(
165        "{}",
166        "║  Spydecy Debugger: C AST Visualization                  ║\n".cyan()
167    ));
168    output.push_str(&format!(
169        "{}",
170        "╚══════════════════════════════════════════════════════════╝\n".cyan()
171    ));
172    output.push('\n');
173
174    // File info
175    output.push_str(&format!("{} {}\n", "File:".bold(), file_path.display()));
176    output.push_str(&format!(
177        "{} {} lines\n",
178        "Size:".bold(),
179        source.lines().count()
180    ));
181    output.push('\n');
182
183    // Source code preview
184    output.push_str(&format!("{}\n", "═══ Source Code ═══".yellow().bold()));
185    for (i, line) in source.lines().enumerate() {
186        output.push_str(&format!("{:3} │ {}\n", (i + 1).to_string().dimmed(), line));
187    }
188    output.push('\n');
189
190    // AST tree with CPython annotations
191    output.push_str(&format!(
192        "{}\n",
193        "═══ Abstract Syntax Tree ═══".green().bold()
194    ));
195    format_c_ast_node(&ast, 0, &mut output);
196    output.push('\n');
197
198    // CPython API analysis
199    output.push_str(&format!(
200        "{}\n",
201        "═══ CPython API Analysis ═══".magenta().bold()
202    ));
203    let cpython_calls = collect_cpython_calls(&ast);
204    if cpython_calls.is_empty() {
205        output.push_str(&format!(
206            "  {} No CPython API calls detected\n",
207            "ℹ".dimmed()
208        ));
209    } else {
210        for (pattern, name) in cpython_calls {
211            output.push_str(&format!(
212                "  {} {} → {:?}\n",
213                "⚡".bright_yellow(),
214                name.bright_white().bold(),
215                pattern
216            ));
217        }
218    }
219    output.push('\n');
220
221    // PyObject tracking
222    output.push_str(&format!("{}\n", "═══ PyObject* Tracking ═══".blue().bold()));
223    let pyobject_params = collect_pyobject_params(&ast);
224    if pyobject_params.is_empty() {
225        output.push_str(&format!(
226            "  {} No PyObject* parameters detected\n",
227            "ℹ".dimmed()
228        ));
229    } else {
230        for (func_name, param_name, param_type) in pyobject_params {
231            output.push_str(&format!(
232                "  {} {}::{} ({})\n",
233                "🐍".bright_cyan(),
234                func_name.yellow(),
235                param_name.bright_white(),
236                param_type.dimmed()
237            ));
238        }
239    }
240    output.push('\n');
241
242    // Statistics
243    output.push_str(&format!("{}\n", "═══ Statistics ═══".blue().bold()));
244    let node_count = count_c_nodes(&ast);
245    output.push_str(&format!("  {} {}\n", "Total AST nodes:".bold(), node_count));
246    output.push_str(&format!(
247        "  {} {}\n",
248        "Root node type:".bold(),
249        ast.node_type
250    ));
251    if !ast.children.is_empty() {
252        output.push_str(&format!(
253            "  {} {}\n",
254            "Direct children:".bold(),
255            ast.children.len()
256        ));
257    }
258
259    Ok(output)
260}
261
262/// Format a C AST node with indentation and `CPython` API highlighting
263fn format_c_ast_node(node: &CAST, depth: usize, output: &mut String) {
264    let indent = "  ".repeat(depth);
265    let connector = if depth > 0 { "├─ " } else { "" };
266    let pattern = cpython::identify_pattern(node);
267
268    // Format node type with color
269    let node_type_colored = colorize_c_node_type(&node.node_type, pattern.is_some());
270    output.push_str(&format!("{indent}{connector}{node_type_colored}"));
271
272    // Add node details
273    format_c_node_details(node, pattern, output);
274    output.push('\n');
275
276    // Recursively format children
277    for child in &node.children {
278        format_c_ast_node(child, depth + 1, output);
279    }
280}
281
282/// Colorize C node type based on type and `CPython` status
283fn colorize_c_node_type(node_type: &str, is_cpython: bool) -> colored::ColoredString {
284    use colored::Colorize;
285
286    match node_type {
287        "TranslationUnit" => node_type.cyan().bold(),
288        "FunctionDecl" if is_cpython => node_type.magenta().bold(),
289        "FunctionDecl" => node_type.green().bold(),
290        "CallExpr" if is_cpython => node_type.magenta(),
291        "CallExpr" => node_type.blue(),
292        "ReturnStmt" => node_type.red(),
293        "VarDecl" => node_type.yellow(),
294        "ParmDecl" => node_type.cyan(),
295        _ => node_type.white(),
296    }
297}
298
299/// Format C node details (name, pattern, return type, parameters)
300fn format_c_node_details(
301    node: &CAST,
302    pattern: Option<cpython::CPythonPattern>,
303    output: &mut String,
304) {
305    // Node name
306    if let Some(ref name) = node.name {
307        output.push_str(&format!(" {}", name.bright_white().bold()));
308    }
309
310    // CPython pattern annotation
311    if let Some(p) = pattern {
312        output.push_str(&format!(" {} {p:?}", "⚡".bright_yellow()));
313    }
314
315    // Return type for functions
316    if let Some(ref ret_type) = node.return_type {
317        output.push_str(&format!(" → {}", ret_type.dimmed()));
318    }
319
320    // Parameters
321    if !node.params.is_empty() {
322        format_c_parameters(&node.params, output);
323    }
324}
325
326/// Format C function parameters with `PyObject` highlighting
327fn format_c_parameters(params: &[spydecy_c::parser::CParam], output: &mut String) {
328    output.push_str(" (");
329    for (i, param) in params.iter().enumerate() {
330        if i > 0 {
331            output.push_str(", ");
332        }
333        format_c_parameter(param, output);
334    }
335    output.push(')');
336}
337
338/// Format a single C parameter with appropriate highlighting
339fn format_c_parameter(param: &spydecy_c::parser::CParam, output: &mut String) {
340    let is_pyobject = param.param_type.contains("PyObject") || param.param_type.contains("PyList");
341    if is_pyobject {
342        output.push_str(&format!(
343            "{}: {}",
344            param.name.bright_cyan().bold(),
345            param.param_type.cyan()
346        ));
347    } else {
348        output.push_str(&format!("{}: {}", param.name, param.param_type.dimmed()));
349    }
350}
351
352/// Collect `CPython` API calls from AST
353fn collect_cpython_calls(node: &CAST) -> Vec<(cpython::CPythonPattern, String)> {
354    let mut calls = Vec::new();
355
356    if let Some(pattern) = cpython::identify_pattern(node) {
357        if let Some(ref name) = node.name {
358            calls.push((pattern, name.clone()));
359        }
360    }
361
362    for child in &node.children {
363        calls.extend(collect_cpython_calls(child));
364    }
365
366    calls
367}
368
369/// Collect `PyObject*` parameters from functions
370fn collect_pyobject_params(node: &CAST) -> Vec<(String, String, String)> {
371    let mut params = Vec::new();
372
373    if node.node_type == "FunctionDecl" {
374        if let Some(ref func_name) = node.name {
375            for param in &node.params {
376                if param.param_type.contains("PyObject")
377                    || param.param_type.contains("PyList")
378                    || param.param_type.contains("PyDict")
379                {
380                    params.push((
381                        func_name.clone(),
382                        param.name.clone(),
383                        param.param_type.clone(),
384                    ));
385                }
386            }
387        }
388    }
389
390    for child in &node.children {
391        params.extend(collect_pyobject_params(child));
392    }
393
394    params
395}
396
397/// Count total nodes in C AST
398fn count_c_nodes(node: &CAST) -> usize {
399    1 + node.children.iter().map(count_c_nodes).sum::<usize>()
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use std::io::Write;
406    use tempfile::NamedTempFile;
407
408    #[test]
409    fn test_visualize_simple_function() {
410        let mut temp_file = NamedTempFile::new().unwrap();
411        writeln!(temp_file, "def my_len(x):\n    return len(x)").unwrap();
412
413        let result = visualize_python(temp_file.path());
414        assert!(result.is_ok());
415
416        let output = result.unwrap();
417        assert!(output.contains("Module"));
418        assert!(output.contains("FunctionDef"));
419        assert!(output.contains("Return"));
420        assert!(output.contains("Call"));
421        assert!(output.contains("my_len"));
422    }
423
424    #[test]
425    fn test_count_nodes() {
426        let ast = PythonAST {
427            node_type: "Module".to_string(),
428            lineno: None,
429            col_offset: None,
430            children: vec![
431                PythonAST::new("FunctionDef".to_string()),
432                PythonAST::new("FunctionDef".to_string()),
433            ],
434            attributes: std::collections::HashMap::new(),
435        };
436
437        assert_eq!(count_nodes(&ast), 3); // Module + 2 FunctionDef
438    }
439
440    #[test]
441    fn test_visualize_simple_c_function() {
442        use std::io::Write;
443        use tempfile::Builder;
444
445        let mut temp_file = Builder::new().suffix(".c").tempfile().unwrap();
446        writeln!(temp_file, "int add(int a, int b) {{\n    return a + b;\n}}").unwrap();
447        temp_file.flush().unwrap(); // Ensure content is written
448
449        let result = visualize_c(temp_file.path());
450        assert!(
451            result.is_ok(),
452            "Should visualize C code: {:?}",
453            result.as_ref().err()
454        );
455
456        let output = result.unwrap();
457        assert!(output.contains("C AST Visualization"));
458        assert!(output.contains("FunctionDecl"));
459        assert!(output.contains("add"));
460    }
461
462    #[test]
463    fn test_visualize_cpython_function() {
464        use std::io::Write;
465        use tempfile::Builder;
466
467        let mut temp_file = Builder::new().suffix(".c").tempfile().unwrap();
468        writeln!(
469            temp_file,
470            "static Py_ssize_t list_length(PyListObject *self) {{\n    return Py_SIZE(self);\n}}"
471        )
472        .unwrap();
473        temp_file.flush().unwrap(); // Ensure content is written
474
475        let result = visualize_c(temp_file.path());
476        assert!(
477            result.is_ok(),
478            "Should visualize CPython code: {:?}",
479            result.as_ref().err()
480        );
481
482        let output = result.unwrap();
483        assert!(output.contains("CPython API Analysis"));
484        assert!(output.contains("PyObject* Tracking"));
485        assert!(output.contains("list_length"));
486    }
487
488    #[test]
489    fn test_collect_cpython_calls() {
490        let mut ast = CAST::new("FunctionDecl".to_owned());
491        ast.name = Some("list_length".to_owned());
492
493        let mut child = CAST::new("CallExpr".to_owned());
494        child.name = Some("PyList_Append".to_owned());
495        ast.children.push(child);
496
497        let calls = collect_cpython_calls(&ast);
498        assert_eq!(calls.len(), 2);
499        assert!(calls.iter().any(|(_, name)| name == "list_length"));
500        assert!(calls.iter().any(|(_, name)| name == "PyList_Append"));
501    }
502
503    #[test]
504    fn test_collect_pyobject_params() {
505        let mut ast = CAST::new("FunctionDecl".to_owned());
506        ast.name = Some("test_func".to_owned());
507        ast.params.push(spydecy_c::parser::CParam {
508            name: "obj".to_owned(),
509            param_type: "PyObject*".to_owned(),
510        });
511        ast.params.push(spydecy_c::parser::CParam {
512            name: "x".to_owned(),
513            param_type: "int".to_owned(),
514        });
515
516        let params = collect_pyobject_params(&ast);
517        assert_eq!(params.len(), 1);
518        assert_eq!(params[0].1, "obj");
519        assert_eq!(params[0].2, "PyObject*");
520    }
521
522    #[test]
523    fn test_count_c_nodes() {
524        let mut ast = CAST::new("TranslationUnit".to_owned());
525        ast.children.push(CAST::new("FunctionDecl".to_owned()));
526        ast.children.push(CAST::new("FunctionDecl".to_owned()));
527
528        assert_eq!(count_c_nodes(&ast), 3); // TranslationUnit + 2 FunctionDecl
529    }
530}