//! Interactive debugger for Lux //! //! Provides breakpoints, stepping, and variable inspection. use crate::ast::{Program, Span}; use crate::interpreter::{Interpreter, Value}; use crate::parser::Parser; use crate::typechecker::TypeChecker; use crate::modules::ModuleLoader; use std::collections::{HashMap, HashSet}; use std::io::{self, Write}; use std::path::Path; /// Debugger state pub struct Debugger { /// Breakpoints by line number breakpoints: HashSet, /// Whether to stop at next statement (single-step mode) single_step: bool, /// Current call stack for display call_stack: Vec, /// Variable values at current scope variables: HashMap, /// Source code lines source_lines: Vec, /// Current line being executed current_line: usize, /// File being debugged file_path: String, } impl Debugger { pub fn new(source: &str, file_path: &str) -> Self { Self { breakpoints: HashSet::new(), single_step: false, call_stack: vec!["
".to_string()], variables: HashMap::new(), source_lines: source.lines().map(String::from).collect(), current_line: 1, file_path: file_path.to_string(), } } /// Run the debugger on a file pub fn run(path: &str) -> Result<(), String> { let file_path = Path::new(path); let source = std::fs::read_to_string(file_path) .map_err(|e| format!("Error reading file '{}': {}", path, e))?; let mut debugger = Debugger::new(&source, path); debugger.debug_session(&source, file_path) } fn debug_session(&mut self, source: &str, file_path: &Path) -> Result<(), String> { println!("Lux Debugger"); println!("Type 'help' for available commands."); println!(); // Parse and type check the program let mut loader = ModuleLoader::new(); if let Some(parent) = file_path.parent() { loader.add_search_path(parent.to_path_buf()); } let program = loader.load_source(source, Some(file_path)) .map_err(|e| format!("Parse error: {}", e))?; let mut checker = TypeChecker::new(); checker.check_program_with_modules(&program, &loader) .map_err(|errors| { errors.iter() .map(|e| e.to_string()) .collect::>() .join("\n") })?; // Show initial state self.show_source_context(1); // Enter debug REPL self.debug_repl(&program, &loader) } fn debug_repl(&mut self, program: &Program, loader: &ModuleLoader) -> Result<(), String> { let mut input = String::new(); loop { print!("(lux-debug) "); io::stdout().flush().unwrap(); input.clear(); if io::stdin().read_line(&mut input).is_err() { break; } let cmd = input.trim(); if cmd.is_empty() { continue; } match self.handle_command(cmd, program, loader) { Ok(true) => continue, Ok(false) => break, Err(e) => println!("Error: {}", e), } } Ok(()) } fn handle_command(&mut self, cmd: &str, program: &Program, loader: &ModuleLoader) -> Result { let parts: Vec<&str> = cmd.split_whitespace().collect(); if parts.is_empty() { return Ok(true); } match parts[0] { "help" | "h" => { self.show_help(); } "quit" | "q" => { return Ok(false); } "run" | "r" => { self.run_program(program, loader)?; } "break" | "b" => { if parts.len() < 2 { println!("Usage: break "); } else if let Ok(line) = parts[1].parse::() { self.add_breakpoint(line); } else { println!("Invalid line number: {}", parts[1]); } } "delete" | "d" => { if parts.len() < 2 { println!("Usage: delete "); } else if let Ok(line) = parts[1].parse::() { self.remove_breakpoint(line); } else { println!("Invalid line number: {}", parts[1]); } } "list" | "l" => { let line = if parts.len() > 1 { parts[1].parse().unwrap_or(self.current_line) } else { self.current_line }; self.show_source_context(line); } "breakpoints" | "bp" => { self.list_breakpoints(); } "step" | "s" => { self.single_step = true; println!("Single-step mode enabled. Use 'run' to execute."); } "continue" | "c" => { self.single_step = false; println!("Continue mode. Will stop at next breakpoint."); } "print" | "p" => { if parts.len() < 2 { println!("Usage: print "); } else { let expr_str = parts[1..].join(" "); self.eval_expression(&expr_str)?; } } "locals" | "vars" => { self.show_variables(); } "stack" | "bt" => { self.show_call_stack(); } _ => { println!("Unknown command: {}. Type 'help' for available commands.", parts[0]); } } Ok(true) } fn show_help(&self) { println!("Debugger Commands:"); println!(" help, h Show this help"); println!(" quit, q Exit debugger"); println!(" run, r Run/continue program"); println!(" break , b Set breakpoint at line"); println!(" delete , d Remove breakpoint"); println!(" breakpoints, bp List all breakpoints"); println!(" list [line], l Show source around line"); println!(" step, s Enable single-step mode"); println!(" continue, c Disable single-step mode"); println!(" print , p Evaluate and print expression"); println!(" locals, vars Show local variables"); println!(" stack, bt Show call stack"); } fn add_breakpoint(&mut self, line: usize) { if line > 0 && line <= self.source_lines.len() { self.breakpoints.insert(line); println!("Breakpoint set at line {}", line); self.show_line(line); } else { println!("Line {} is out of range (1-{})", line, self.source_lines.len()); } } fn remove_breakpoint(&mut self, line: usize) { if self.breakpoints.remove(&line) { println!("Breakpoint removed at line {}", line); } else { println!("No breakpoint at line {}", line); } } fn list_breakpoints(&self) { if self.breakpoints.is_empty() { println!("No breakpoints set."); } else { println!("Breakpoints:"); let mut lines: Vec<_> = self.breakpoints.iter().collect(); lines.sort(); for line in lines { print!(" "); self.show_line(*line); } } } fn show_source_context(&self, center_line: usize) { let start = center_line.saturating_sub(3).max(1); let end = (center_line + 3).min(self.source_lines.len()); println!(); for line_num in start..=end { self.show_line_with_marker(line_num, line_num == center_line); } println!(); } fn show_line(&self, line_num: usize) { self.show_line_with_marker(line_num, false); } fn show_line_with_marker(&self, line_num: usize, is_current: bool) { if line_num > 0 && line_num <= self.source_lines.len() { let bp_marker = if self.breakpoints.contains(&line_num) { "*" } else { " " }; let cur_marker = if is_current { ">" } else { " " }; println!( "{}{} {:4} | {}", bp_marker, cur_marker, line_num, self.source_lines[line_num - 1] ); } } fn run_program(&mut self, program: &Program, loader: &ModuleLoader) -> Result<(), String> { println!("Running {}...", self.file_path); println!(); let mut interp = Interpreter::new(); match interp.run_with_modules(program, loader) { Ok(value) => { if !matches!(value, Value::Unit) { println!(); println!("Result: {}", value); } println!(); println!("Program finished."); } Err(e) => { println!(); println!("Runtime error: {}", e.message); if let Some(span) = e.span { // Try to find the line from the span let line = self.span_to_line(span); self.current_line = line; self.show_source_context(line); } } } Ok(()) } fn span_to_line(&self, span: Span) -> usize { // Count newlines in source up to span.start let source: String = self.source_lines.join("\n"); let mut line = 1; for (i, c) in source.chars().enumerate() { if i >= span.start { break; } if c == '\n' { line += 1; } } line } fn eval_expression(&mut self, expr_str: &str) -> Result<(), String> { // Parse and evaluate the expression by creating a simple program use crate::lexer::Lexer; // Create a mini program to evaluate: just the expression as a top-level binding let source = format!("let __debug_result__ = {}", expr_str); let lexer = Lexer::new(&source); let tokens = lexer.tokenize() .map_err(|e| format!("Lexer error: {}", e.message))?; let mut parser = Parser::new(tokens); let program = parser.parse_program() .map_err(|e| format!("Parse error: {}", e.message))?; let mut interp = Interpreter::new(); match interp.run(&program) { Ok(value) => { println!("{}", value); } Err(e) => { println!("Error: {}", e.message); } } Ok(()) } fn show_variables(&self) { if self.variables.is_empty() { println!("No variables in current scope."); println!("(Run the program first to capture variable state)"); } else { println!("Local variables:"); for (name, value) in &self.variables { println!(" {} = {}", name, value); } } } fn show_call_stack(&self) { println!("Call stack:"); for (i, frame) in self.call_stack.iter().enumerate().rev() { let marker = if i == self.call_stack.len() - 1 { ">" } else { " " }; println!(" {} #{} {}", marker, i, frame); } } }