use itertools::Itertools;
use super::runtime_value::RuntimeValue;
use crate::ast::node as ast;
use crate::eval::Evaluator;
use crate::eval::env::Env;
use crate::{ModuleResolver, Shared, SharedCell, Token};
use std::{collections::HashSet, fmt::Debug};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)]
pub enum DebuggerCommand {
#[default]
Continue,
StepInto,
StepOver,
Next,
FunctionExit,
Quit,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Breakpoint {
pub id: usize,
pub line: usize,
pub column: Option<usize>,
pub enabled: bool,
pub source: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Source {
pub name: Option<String>,
pub code: String,
}
#[derive(Debug, Clone)]
pub struct DebugContext {
pub current_value: RuntimeValue,
pub current_node: Shared<ast::Node>,
pub token: Shared<Token>,
pub call_stack: Vec<Shared<ast::Node>>,
pub env: Shared<SharedCell<Env>>,
pub source: Source,
}
impl Default for DebugContext {
fn default() -> Self {
Self {
current_value: RuntimeValue::NONE,
current_node: Shared::new(ast::Node {
token_id: crate::ast::TokenId::new(0),
expr: Shared::new(ast::Expr::Literal(ast::Literal::Number(0.0.into()))),
}),
token: Shared::new(Token {
kind: crate::TokenKind::Eof,
range: crate::Range::default(),
module_id: crate::eval::module::ModuleId::new(0),
}),
call_stack: Vec::new(),
env: Shared::new(SharedCell::new(Env::default())),
source: Source::default(),
}
}
}
#[derive(Debug)]
pub struct Debugger {
breakpoints: HashSet<Breakpoint>,
call_stack: Vec<Shared<ast::Node>>,
next_breakpoint_id: usize,
current_command: DebuggerCommand,
active: bool,
step_depth: Option<usize>,
}
impl Default for Debugger {
fn default() -> Self {
Self::new()
}
}
impl Debugger {
pub fn new() -> Self {
Self {
breakpoints: HashSet::new(),
call_stack: Vec::new(),
next_breakpoint_id: 1,
current_command: DebuggerCommand::Continue,
active: false,
step_depth: None,
}
}
pub fn activate(&mut self) {
self.active = true;
}
pub fn deactivate(&mut self) {
self.active = false;
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn add_breakpoint(&mut self, line: usize, column: Option<usize>, source: Option<String>) -> usize {
let breakpoint = Breakpoint {
id: self.next_breakpoint_id,
line,
column,
enabled: true,
source,
};
let id = breakpoint.id;
self.breakpoints.insert(breakpoint);
self.next_breakpoint_id += 1;
id
}
pub fn push_call_stack(&mut self, node: Shared<ast::Node>) {
self.call_stack.push(node);
}
pub fn pop_call_stack(&mut self) {
self.call_stack.pop();
}
pub fn current_call_stack(&self) -> Vec<Shared<ast::Node>> {
self.call_stack.clone()
}
pub fn remove_breakpoint(&mut self, id: usize) -> bool {
self.breakpoints.retain(|bp| bp.id != id);
true
}
pub fn remove_breakpoints(&mut self, source: &Option<String>) -> bool {
self.breakpoints.retain(|bp| bp.source != *source);
true
}
pub fn list_breakpoints(&self) -> Vec<&Breakpoint> {
self.breakpoints.iter().collect()
}
pub fn set_command(&mut self, command: DebuggerCommand) {
self.current_command = command;
match command {
DebuggerCommand::StepInto | DebuggerCommand::StepOver | DebuggerCommand::Next => {
self.step_depth = None;
}
DebuggerCommand::FunctionExit | DebuggerCommand::Quit => {
}
DebuggerCommand::Continue => {
self.step_depth = None;
}
}
}
pub fn get_hit_breakpoint(&self, context: &DebugContext, token: Shared<Token>) -> Option<Breakpoint> {
if !self.active {
return None;
}
let line = token.range.start.line as usize;
let column = token.range.start.column;
self.find_active_breakpoint(line, column, &context.source)
}
pub fn next(&mut self, next_action: DebuggerAction) {
self.handle_debugger_action(next_action.clone());
self.current_command = next_action.clone().into();
}
pub fn should_break(&mut self, context: &DebugContext) -> bool {
if !self.active {
return false;
}
match self.current_command {
DebuggerCommand::Continue => false,
DebuggerCommand::Quit => {
self.deactivate();
false
}
DebuggerCommand::StepInto => {
self.current_command = DebuggerCommand::Continue;
true
}
DebuggerCommand::StepOver => {
if let Some(step_depth) = self.step_depth {
if context.call_stack.len() <= step_depth {
self.current_command = DebuggerCommand::Continue;
self.step_depth = None;
true
} else {
false
}
} else {
self.step_depth = Some(context.call_stack.len());
self.current_command = DebuggerCommand::Continue;
true
}
}
DebuggerCommand::Next => {
if let Some(step_depth) = self.step_depth {
if context.call_stack.len() <= step_depth {
self.current_command = DebuggerCommand::Continue;
self.step_depth = None;
true
} else {
false
}
} else {
self.step_depth = Some(context.call_stack.len());
self.current_command = DebuggerCommand::Continue;
true
}
}
DebuggerCommand::FunctionExit => {
if let Some(step_depth) = self.step_depth {
if context.call_stack.len() < step_depth {
self.current_command = DebuggerCommand::Continue;
self.step_depth = None;
true
} else {
false
}
} else {
self.step_depth = Some(context.call_stack.len().saturating_sub(1));
false
}
}
}
}
fn handle_debugger_action(&mut self, action: DebuggerAction) {
match action {
DebuggerAction::Breakpoint(Some(line_no)) => {
self.add_breakpoint(line_no, None, None);
}
DebuggerAction::Breakpoint(None) => {
println!(
"Breakpoints:\n{}",
self.breakpoints
.iter()
.sorted_by_key(|bp| (bp.line, bp.column))
.map(|bp| {
format!(
" [{}] {}:{}{}",
bp.id,
bp.line,
bp.column.map(|col| col.to_string()).unwrap_or_else(|| "-".to_string()),
if bp.enabled { " (enabled)" } else { " (disabled)" },
)
})
.join("\n")
);
}
DebuggerAction::Clear(Some(breakpoint_id)) => {
self.remove_breakpoint(breakpoint_id);
}
DebuggerAction::Clear(None) => {
self.clear_breakpoints();
}
DebuggerAction::Continue
| DebuggerAction::Next
| DebuggerAction::StepInto
| DebuggerAction::StepOver
| DebuggerAction::FunctionExit
| DebuggerAction::Quit => {}
}
}
fn find_active_breakpoint(&self, line: usize, column: usize, source: &Source) -> Option<Breakpoint> {
for breakpoint in &self.breakpoints {
if !breakpoint.enabled {
continue;
}
if breakpoint.source != source.name {
continue;
}
if breakpoint.line == line {
if let Some(bp_column) = breakpoint.column
&& bp_column != column
{
continue;
}
return Some(breakpoint.clone());
}
}
None
}
pub fn current_command(&self) -> DebuggerCommand {
self.current_command
}
pub fn clear_breakpoints(&mut self) {
self.breakpoints.clear();
}
}
type LineNo = usize;
type BreakpointId = usize;
#[derive(Debug, Clone, PartialEq)]
pub enum DebuggerAction {
Breakpoint(Option<LineNo>),
Continue,
Clear(Option<BreakpointId>),
StepInto,
StepOver,
Next,
FunctionExit,
Quit,
}
impl From<DebuggerCommand> for DebuggerAction {
fn from(command: DebuggerCommand) -> Self {
match command {
DebuggerCommand::Continue => DebuggerAction::Continue,
DebuggerCommand::StepInto => DebuggerAction::StepInto,
DebuggerCommand::StepOver => DebuggerAction::StepOver,
DebuggerCommand::Next => DebuggerAction::Next,
DebuggerCommand::FunctionExit => DebuggerAction::FunctionExit,
DebuggerCommand::Quit => DebuggerAction::Quit,
}
}
}
impl From<DebuggerAction> for DebuggerCommand {
fn from(action: DebuggerAction) -> Self {
match action {
DebuggerAction::Breakpoint(_) => DebuggerCommand::Continue,
DebuggerAction::Clear(_) => DebuggerCommand::Continue,
DebuggerAction::Continue => DebuggerCommand::Continue,
DebuggerAction::StepInto => DebuggerCommand::StepInto,
DebuggerAction::StepOver => DebuggerCommand::StepOver,
DebuggerAction::Next => DebuggerCommand::Next,
DebuggerAction::FunctionExit => DebuggerCommand::FunctionExit,
DebuggerAction::Quit => DebuggerCommand::Quit,
}
}
}
pub trait DebuggerHandler: std::fmt::Debug + Send + Sync {
fn on_breakpoint_hit(&self, _breakpoint: &Breakpoint, _context: &DebugContext) -> DebuggerAction {
DebuggerAction::Continue
}
fn on_step(&self, _context: &DebugContext) -> DebuggerAction {
DebuggerAction::Continue
}
}
#[derive(Debug, Default)]
pub struct DefaultDebuggerHandler;
impl DebuggerHandler for DefaultDebuggerHandler {}
impl<T: ModuleResolver> Evaluator<T> {
pub fn debugger(&self) -> Shared<SharedCell<Debugger>> {
Shared::clone(&self.debugger)
}
pub fn set_debugger_handler(&mut self, handler: Box<dyn DebuggerHandler>) {
self.debugger_handler = Shared::new(SharedCell::new(handler));
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::{Arena, Range, TokenKind, ast::TokenId, eval::module::ModuleId};
use super::*;
fn make_token(line: usize, column: usize) -> Shared<Token> {
Shared::new(Token {
kind: TokenKind::Ident("dummy".into()),
range: Range {
start: crate::Position {
line: line as u32,
column,
},
end: crate::Position {
line: line as u32,
column: column + 1,
},
},
module_id: ModuleId::new(0),
})
}
fn make_node(token_id: TokenId) -> Shared<ast::Node> {
Shared::new(ast::Node {
token_id,
expr: Shared::new(ast::Expr::Literal(ast::Literal::Number(42.0.into()))),
})
}
fn make_debug_context(line: usize, column: usize) -> DebugContext {
let mut arena = Arena::new(10);
let token = make_token(line, column);
let token_id = arena.alloc(Shared::clone(&token));
let node = make_node(token_id);
DebugContext {
current_value: RuntimeValue::NONE,
current_node: node,
token: Shared::clone(&token),
call_stack: Vec::new(),
env: Shared::new(SharedCell::new(Env::default())),
source: Source::default(),
}
}
#[rstest]
#[case(DebuggerCommand::Continue, false, "Continue: should not break")]
#[case(DebuggerCommand::Quit, false, "Quit: should not break and deactivate")]
#[case(DebuggerCommand::StepInto, true, "StepInto: should break once")]
fn test_should_break_basic(#[case] command: DebuggerCommand, #[case] expected_hit: bool, #[case] _desc: &str) {
let mut dbg = Debugger::new();
dbg.activate();
dbg.set_command(command);
let ctx = make_debug_context(1, 1);
let hit = dbg.should_break(&ctx);
assert_eq!(hit, expected_hit);
if command == DebuggerCommand::Quit {
assert!(!dbg.is_active(), "Debugger should be deactivated after Quit");
}
}
#[rstest]
#[case(
DebuggerCommand::StepOver,
None,
vec![],
true,
"StepOver: step_depth None, call_stack empty"
)]
#[case(
DebuggerCommand::StepOver,
Some(1),
vec![0, 1],
false,
"StepOver: step_depth Some(1), call_stack.len()=2"
)]
#[case(
DebuggerCommand::StepOver,
Some(2),
vec![0],
true,
"StepOver: step_depth Some(2), call_stack.len()=1"
)]
#[case(
DebuggerCommand::Next,
None,
vec![],
true,
"Next: step_depth None, call_stack empty"
)]
#[case(
DebuggerCommand::Next,
Some(1),
vec![0, 1],
false,
"Next: step_depth Some(1), call_stack.len()=2"
)]
#[case(
DebuggerCommand::Next,
Some(2),
vec![0],
true,
"Next: step_depth Some(2), call_stack.len()=1"
)]
#[case(
DebuggerCommand::FunctionExit,
None,
vec![],
false,
"FunctionExit: step_depth None, call_stack empty"
)]
#[case(
DebuggerCommand::FunctionExit,
Some(2),
vec![0],
true,
"FunctionExit: step_depth Some(2), call_stack.len()=1"
)]
#[case(
DebuggerCommand::FunctionExit,
Some(1),
vec![0, 1],
false,
"FunctionExit: step_depth Some(1), call_stack.len()=2"
)]
fn test_should_break_with_step_depth(
#[case] command: DebuggerCommand,
#[case] step_depth: Option<usize>,
#[case] call_stack_indices: Vec<usize>,
#[case] expected_hit: bool,
#[case] _desc: &str,
) {
let mut dbg = Debugger::new();
dbg.activate();
dbg.set_command(command);
dbg.step_depth = step_depth;
let ctx = make_debug_context(1, 1);
let mut ctx = ctx;
ctx.call_stack = call_stack_indices
.into_iter()
.map(|i| make_node(TokenId::new(i as u32)))
.collect();
let hit = dbg.should_break(&ctx);
assert_eq!(hit, expected_hit);
}
#[test]
fn test_add_and_remove_breakpoint() {
let mut dbg = Debugger::new();
let id = dbg.add_breakpoint(1, Some(2), None);
assert_eq!(dbg.list_breakpoints().len(), 1);
assert!(dbg.remove_breakpoint(id));
assert_eq!(dbg.list_breakpoints().len(), 0);
}
#[test]
fn test_clear_breakpoints() {
let mut dbg = Debugger::new();
dbg.add_breakpoint(1, None, None);
dbg.add_breakpoint(2, Some(3), None);
assert_eq!(dbg.list_breakpoints().len(), 2);
dbg.clear_breakpoints();
assert_eq!(dbg.list_breakpoints().len(), 0);
}
#[test]
fn test_debugger_activate_deactivate() {
let mut dbg = Debugger::new();
assert!(!dbg.is_active());
dbg.activate();
assert!(dbg.is_active());
dbg.deactivate();
assert!(!dbg.is_active());
}
}