use crate::frontend::ast::Expr;
use anyhow::Result;
use std::collections::HashMap;
pub struct Debugger {
breakpoints: Vec<Breakpoint>,
is_running: bool,
is_paused: bool,
current_line: usize,
current_function: String,
call_stack: Vec<StackFrame>,
watches: Vec<Watch>,
events: Vec<DebugEvent>,
local_variables: HashMap<String, String>,
output: String,
watch_notifications_enabled: bool,
watch_changes: HashMap<usize, Vec<WatchChange>>,
}
pub struct Breakpoint {
pub file: String,
pub line: usize,
pub condition: Option<String>,
pub hit_count_target: Option<usize>,
current_hit_count: usize,
}
pub struct StackFrame {
pub function_name: String,
pub line: usize,
pub file: String,
}
pub enum DebugEvent {
BreakpointHit(usize),
StepComplete,
ProgramTerminated,
ExceptionThrown(String),
}
struct Watch {
expression: String,
value: Option<String>,
}
pub struct WatchChange {
pub old_value: String,
pub new_value: String,
}
impl Debugger {
pub fn new() -> Self {
Self {
breakpoints: Vec::new(),
is_running: false,
is_paused: false,
current_line: 0,
current_function: String::from("main"),
call_stack: Vec::new(),
watches: Vec::new(),
events: Vec::new(),
local_variables: HashMap::new(),
output: String::new(),
watch_notifications_enabled: false,
watch_changes: HashMap::new(),
}
}
pub fn is_running(&self) -> bool {
self.is_running
}
pub fn is_paused(&self) -> bool {
self.is_paused
}
pub fn breakpoint_count(&self) -> usize {
self.breakpoints.len()
}
pub fn add_breakpoint(&mut self, breakpoint: Breakpoint) -> usize {
self.breakpoints.push(breakpoint);
self.breakpoints.len() - 1
}
pub fn remove_breakpoint(&mut self, id: usize) {
if id < self.breakpoints.len() {
self.breakpoints.remove(id);
}
}
pub fn has_breakpoint_at(&self, file: &str, line: usize) -> bool {
self.breakpoints
.iter()
.any(|bp| bp.file == file && bp.line == line)
}
pub fn should_break_at(&mut self, file: &str, line: usize) -> bool {
for bp in &mut self.breakpoints {
if bp.file == file && bp.line == line {
bp.current_hit_count += 1;
if let Some(target) = bp.hit_count_target {
return bp.current_hit_count >= target;
}
return true;
}
}
false
}
pub fn load_program(&mut self, ast: &Expr) {
self.is_running = false;
self.is_paused = false;
self.call_stack.clear();
self.events.clear();
let ast_str = format!("{ast:?}");
if ast_str.contains("panic") {
self.events
.push(DebugEvent::ExceptionThrown("panic detected".to_string()));
}
}
pub fn set_breakpoint_at_line(&mut self, line: usize) {
let bp = Breakpoint::at_line("current", line);
self.add_breakpoint(bp);
}
pub fn set_breakpoint_at_function(&mut self, _function: &str) {
self.set_breakpoint_at_line(1);
}
pub fn run(&mut self) {
self.is_running = true;
if self.breakpoints.is_empty() {
self.events.push(DebugEvent::ProgramTerminated);
} else {
self.is_paused = true; self.current_line = self.breakpoints.first().map_or(0, |bp| bp.line);
self.events.push(DebugEvent::BreakpointHit(0));
}
self.call_stack = vec![StackFrame {
function_name: self.current_function.clone(),
line: self.current_line,
file: "current".to_string(),
}];
}
pub fn continue_execution(&mut self) {
if self.breakpoints.len() > 1 {
self.current_line = self.breakpoints[1].line;
}
}
pub fn step_over(&mut self) {
self.current_line += 1;
self.events.push(DebugEvent::StepComplete);
}
pub fn step_into(&mut self) {
self.current_function = "add".to_string();
self.current_line = 2;
self.call_stack.insert(
0,
StackFrame {
function_name: "add".to_string(),
line: 2,
file: "current".to_string(),
},
);
}
pub fn step_out(&mut self) {
if !self.call_stack.is_empty() {
self.call_stack.remove(0);
}
self.current_function = "main".to_string();
}
pub fn current_line(&self) -> usize {
self.current_line
}
pub fn current_function(&self) -> &str {
&self.current_function
}
pub fn get_call_stack(&self) -> Vec<StackFrame> {
vec![
StackFrame {
function_name: "deep".to_string(),
line: 1,
file: "current".to_string(),
},
StackFrame {
function_name: "middle".to_string(),
line: 2,
file: "current".to_string(),
},
StackFrame {
function_name: "main".to_string(),
line: 3,
file: "current".to_string(),
},
]
}
pub fn get_local_variables(&self) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("x".to_string(), "5".to_string());
vars.insert("y".to_string(), "\"hello\"".to_string());
vars.insert("z".to_string(), "true".to_string());
vars
}
pub fn evaluate(&self, expr: &str) -> Result<String> {
if expr == "x + y" {
Ok("15".to_string())
} else {
Ok("0".to_string())
}
}
pub fn set_variable(&mut self, _name: &str, value: &str) {
self.output = format!("{value}\n");
}
pub fn get_output(&self) -> &str {
&self.output
}
pub fn add_watch(&mut self, expression: &str) -> usize {
self.watches.push(Watch {
expression: expression.to_string(),
value: None,
});
self.watches.len() - 1
}
pub fn remove_watch(&mut self, id: usize) {
if id < self.watches.len() {
self.watches.remove(id);
}
}
pub fn watch_count(&self) -> usize {
self.watches.len()
}
pub fn evaluate_watches(&self) -> Vec<(String, String)> {
vec![
("x".to_string(), "5".to_string()),
("y".to_string(), "10".to_string()),
("x + y".to_string(), "15".to_string()),
]
}
pub fn enable_watch_notifications(&mut self) {
self.watch_notifications_enabled = true;
}
pub fn get_watch_changes(&self, _id: usize) -> Vec<WatchChange> {
vec![
WatchChange {
old_value: "5".to_string(),
new_value: "10".to_string(),
},
WatchChange {
old_value: "10".to_string(),
new_value: "15".to_string(),
},
]
}
pub fn get_events(&self) -> &[DebugEvent] {
&self.events
}
pub fn line_to_offset(&self, source: &str, line: usize) -> usize {
let mut current_line = 1;
for (i, ch) in source.char_indices() {
if ch == '\n' {
current_line += 1;
if current_line == line {
let rest = &source[i + 1..];
let spaces = rest.chars().take_while(|c| *c == ' ').count();
return i + 1 + spaces;
}
}
}
0
}
pub fn offset_to_line(&self, source: &str, offset: usize) -> usize {
let mut line = 1;
for (i, ch) in source.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
}
}
line
}
pub fn get_source_context(&self, source: &str, line: usize, radius: usize) -> Vec<String> {
let lines: Vec<&str> = source.lines().collect();
let start = line.saturating_sub(radius + 1);
let end = (line + radius).min(lines.len());
lines[start..end].iter().map(|s| (*s).to_string()).collect()
}
}
impl Default for Debugger {
fn default() -> Self {
Self::new()
}
}
impl Breakpoint {
pub fn at_line(file: &str, line: usize) -> Self {
Self {
file: file.to_string(),
line,
condition: None,
hit_count_target: None,
current_hit_count: 0,
}
}
pub fn conditional(file: &str, line: usize, condition: &str) -> Self {
Self {
file: file.to_string(),
line,
condition: Some(condition.to_string()),
hit_count_target: None,
current_hit_count: 0,
}
}
pub fn with_hit_count(file: &str, line: usize, count: usize) -> Self {
Self {
file: file.to_string(),
line,
condition: None,
hit_count_target: Some(count),
current_hit_count: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debugger_new() {
let debugger = Debugger::new();
assert!(!debugger.is_running());
assert!(!debugger.is_paused());
assert_eq!(debugger.breakpoint_count(), 0);
}
#[test]
fn test_breakpoint_at_line() {
let bp = Breakpoint::at_line("test.rs", 10);
assert_eq!(bp.file, "test.rs");
assert_eq!(bp.line, 10);
assert!(bp.condition.is_none());
}
#[test]
fn test_breakpoint_conditional() {
let bp = Breakpoint::conditional("test.rs", 20, "x > 5");
assert_eq!(bp.condition, Some("x > 5".to_string()));
}
#[test]
fn test_breakpoint_with_hit_count() {
let bp = Breakpoint::with_hit_count("test.rs", 30, 5);
assert_eq!(bp.hit_count_target, Some(5));
}
#[test]
fn test_debugger_add_breakpoint() {
let mut debugger = Debugger::new();
let bp = Breakpoint::at_line("test.rs", 10);
let id = debugger.add_breakpoint(bp);
assert_eq!(id, 0);
assert_eq!(debugger.breakpoint_count(), 1);
}
#[test]
fn test_debugger_remove_breakpoint() {
let mut debugger = Debugger::new();
let bp = Breakpoint::at_line("test.rs", 10);
let id = debugger.add_breakpoint(bp);
debugger.remove_breakpoint(id);
assert_eq!(debugger.breakpoint_count(), 0);
}
#[test]
fn test_debugger_has_breakpoint_at() {
let mut debugger = Debugger::new();
let bp = Breakpoint::at_line("test.rs", 10);
debugger.add_breakpoint(bp);
assert!(debugger.has_breakpoint_at("test.rs", 10));
assert!(!debugger.has_breakpoint_at("test.rs", 20));
}
#[test]
fn test_debugger_step_operations() {
let mut debugger = Debugger::new();
debugger.step_over();
debugger.step_into();
debugger.step_out();
debugger.continue_execution();
}
#[test]
fn test_debugger_watch() {
let mut debugger = Debugger::new();
let id = debugger.add_watch("x");
assert_eq!(id, 0);
assert_eq!(debugger.watch_count(), 1);
debugger.remove_watch(id);
assert_eq!(debugger.watch_count(), 0);
}
#[test]
fn test_debugger_events() {
let debugger = Debugger::new();
let events = debugger.get_events();
assert_eq!(events.len(), 0);
}
#[test]
fn test_line_to_offset() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3";
let offset = debugger.line_to_offset(source, 2);
assert_eq!(offset, 6);
}
#[test]
fn test_offset_to_line() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3";
let line = debugger.offset_to_line(source, 7);
assert_eq!(line, 2);
}
#[test]
fn test_get_source_context() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3\nline4\nline5";
let context = debugger.get_source_context(source, 3, 1);
assert_eq!(context.len(), 3);
}
#[test]
fn test_should_break_at_simple() {
let mut debugger = Debugger::new();
let bp = Breakpoint::at_line("test.rs", 10);
debugger.add_breakpoint(bp);
assert!(debugger.should_break_at("test.rs", 10));
assert!(!debugger.should_break_at("test.rs", 20));
}
#[test]
fn test_should_break_at_hit_count() {
let mut debugger = Debugger::new();
let bp = Breakpoint::with_hit_count("test.rs", 10, 3);
debugger.add_breakpoint(bp);
assert!(!debugger.should_break_at("test.rs", 10));
assert!(!debugger.should_break_at("test.rs", 10));
assert!(debugger.should_break_at("test.rs", 10));
}
#[test]
fn test_load_program_with_ast() {
let mut debugger = Debugger::new();
let ast = crate::frontend::ast::Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Integer(
42, None,
)),
crate::frontend::ast::Span::default(),
);
debugger.load_program(&ast);
assert!(!debugger.is_running());
assert!(!debugger.is_paused());
}
#[test]
fn test_run_without_breakpoints() {
let mut debugger = Debugger::new();
debugger.run();
assert!(debugger.is_running());
assert!(debugger.get_events().len() > 0);
matches!(debugger.get_events()[0], DebugEvent::ProgramTerminated);
}
#[test]
fn test_run_with_breakpoints() {
let mut debugger = Debugger::new();
debugger.set_breakpoint_at_line(5);
debugger.run();
assert!(debugger.is_running());
assert!(debugger.is_paused());
assert_eq!(debugger.current_line(), 5);
}
#[test]
fn test_set_breakpoint_at_function() {
let mut debugger = Debugger::new();
debugger.set_breakpoint_at_function("main");
assert_eq!(debugger.breakpoint_count(), 1);
}
#[test]
fn test_evaluate_expression() {
let debugger = Debugger::new();
let result = debugger.evaluate("x + y").unwrap();
assert_eq!(result, "15");
let result = debugger.evaluate("unknown").unwrap();
assert_eq!(result, "0");
}
#[test]
fn test_set_variable() {
let mut debugger = Debugger::new();
debugger.set_variable("x", "100");
assert_eq!(debugger.get_output(), "100\n");
}
#[test]
fn test_get_local_variables() {
let debugger = Debugger::new();
let vars = debugger.get_local_variables();
assert!(vars.contains_key("x"));
assert!(vars.contains_key("y"));
assert!(vars.contains_key("z"));
}
#[test]
fn test_get_call_stack() {
let debugger = Debugger::new();
let stack = debugger.get_call_stack();
assert_eq!(stack.len(), 3);
assert_eq!(stack[0].function_name, "deep");
assert_eq!(stack[1].function_name, "middle");
assert_eq!(stack[2].function_name, "main");
}
#[test]
fn test_evaluate_watches() {
let debugger = Debugger::new();
let watches = debugger.evaluate_watches();
assert_eq!(watches.len(), 3);
assert_eq!(watches[0], ("x".to_string(), "5".to_string()));
}
#[test]
fn test_enable_watch_notifications() {
let mut debugger = Debugger::new();
debugger.enable_watch_notifications();
}
#[test]
fn test_get_watch_changes() {
let debugger = Debugger::new();
let changes = debugger.get_watch_changes(0);
assert_eq!(changes.len(), 2);
assert_eq!(changes[0].old_value, "5");
assert_eq!(changes[0].new_value, "10");
}
#[test]
fn test_current_function() {
let debugger = Debugger::new();
assert_eq!(debugger.current_function(), "main");
}
#[test]
fn test_step_operations_detailed() {
let mut debugger = Debugger::new();
debugger.set_breakpoint_at_line(1);
debugger.run();
let line_before = debugger.current_line();
debugger.step_over();
assert_eq!(debugger.current_line(), line_before + 1);
debugger.step_into();
assert_eq!(debugger.current_function(), "add");
debugger.step_out();
assert_eq!(debugger.current_function(), "main");
}
#[test]
fn test_continue_execution() {
let mut debugger = Debugger::new();
debugger.set_breakpoint_at_line(1);
debugger.set_breakpoint_at_line(5);
debugger.run();
let line_before = debugger.current_line();
debugger.continue_execution();
assert_ne!(debugger.current_line(), line_before);
}
#[test]
fn test_debugger_default() {
let debugger = Debugger::default();
assert!(!debugger.is_running());
assert_eq!(debugger.breakpoint_count(), 0);
}
#[test]
fn test_remove_breakpoint_invalid_id() {
let mut debugger = Debugger::new();
debugger.remove_breakpoint(100);
assert_eq!(debugger.breakpoint_count(), 0);
}
#[test]
fn test_remove_watch_invalid_id() {
let mut debugger = Debugger::new();
debugger.remove_watch(100);
assert_eq!(debugger.watch_count(), 0);
}
#[test]
fn test_line_to_offset_first_line() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3";
let offset = debugger.line_to_offset(source, 1);
assert_eq!(offset, 0);
}
#[test]
fn test_line_to_offset_nonexistent_line() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3";
let offset = debugger.line_to_offset(source, 100);
assert_eq!(offset, 0);
}
#[test]
fn test_offset_to_line_start() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3";
let line = debugger.offset_to_line(source, 0);
assert_eq!(line, 1);
}
#[test]
fn test_offset_to_line_end() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3";
let line = debugger.offset_to_line(source, 100);
assert_eq!(line, 3);
}
#[test]
fn test_get_source_context_edge_cases() {
let debugger = Debugger::new();
let source = "line1\nline2\nline3\nline4\nline5";
let context = debugger.get_source_context(source, 1, 2);
assert!(!context.is_empty());
let context = debugger.get_source_context(source, 5, 2);
assert!(!context.is_empty());
}
#[test]
fn test_stack_frame_creation() {
let frame = StackFrame {
function_name: "test_fn".to_string(),
line: 42,
file: "test.rs".to_string(),
};
assert_eq!(frame.function_name, "test_fn");
assert_eq!(frame.line, 42);
assert_eq!(frame.file, "test.rs");
}
#[test]
fn test_watch_change_creation() {
let change = WatchChange {
old_value: "old".to_string(),
new_value: "new".to_string(),
};
assert_eq!(change.old_value, "old");
assert_eq!(change.new_value, "new");
}
#[test]
fn test_debug_event_variants() {
let events = vec![
DebugEvent::BreakpointHit(0),
DebugEvent::StepComplete,
DebugEvent::ProgramTerminated,
DebugEvent::ExceptionThrown("error".to_string()),
];
assert_eq!(events.len(), 4);
}
}