use std::collections::HashMap;
use crate::builtins::error::call_stack_depth;
use crate::bytecode::bytecode_array::BytecodeArray;
use crate::error::StatorError;
pub type BreakpointId = u32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PauseReason {
DebuggerStatement,
Breakpoint(BreakpointId),
Step,
Exception,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DebugAction {
Continue,
StepOver,
StepInto,
StepOut,
}
#[derive(Debug, Clone)]
pub struct Breakpoint {
pub id: BreakpointId,
pub bytecode_offset: u32,
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BreakpointLocation {
pub bytecode_offset: u32,
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StepMode {
None,
Into,
Over { depth: usize },
Out { depth: usize },
}
pub struct Debugger {
breakpoints: HashMap<u32, Breakpoint>,
next_id: BreakpointId,
pub pause_on_exceptions: bool,
step_mode: StepMode,
skip_next: bool,
exception_resume: bool,
last_pause_reason: Option<PauseReason>,
last_pause_offset: u32,
}
impl Default for Debugger {
fn default() -> Self {
Self::new()
}
}
impl Debugger {
pub fn new() -> Self {
Self {
breakpoints: HashMap::new(),
next_id: 1,
pause_on_exceptions: false,
step_mode: StepMode::None,
skip_next: false,
exception_resume: false,
last_pause_reason: None,
last_pause_offset: 0,
}
}
pub fn set_breakpoint_at_offset(
&mut self,
offset: u32,
line: u32,
column: u32,
) -> BreakpointId {
let id = self.next_id;
self.next_id += 1;
self.breakpoints.insert(
offset,
Breakpoint {
id,
bytecode_offset: offset,
line,
column,
},
);
id
}
pub fn set_breakpoint_at_line(
&mut self,
bytecodes: &BytecodeArray,
line: u32,
) -> Option<BreakpointId> {
let pos = bytecodes
.source_positions()
.iter()
.find(|p| p.line == line)?;
Some(self.set_breakpoint_at_offset(pos.bytecode_offset, pos.line, pos.column))
}
pub fn remove_breakpoint(&mut self, id: BreakpointId) -> bool {
let before = self.breakpoints.len();
self.breakpoints.retain(|_, bp| bp.id != id);
self.breakpoints.len() < before
}
pub fn breakpoints(&self) -> impl Iterator<Item = &Breakpoint> {
self.breakpoints.values()
}
pub fn breakpoint_locations(bytecodes: &BytecodeArray) -> Vec<BreakpointLocation> {
bytecodes
.source_positions()
.iter()
.map(|p| BreakpointLocation {
bytecode_offset: p.bytecode_offset,
line: p.line,
column: p.column,
})
.collect()
}
pub fn set_pause_on_exceptions(&mut self, enable: bool) {
self.pause_on_exceptions = enable;
}
pub fn last_pause_reason(&self) -> Option<&PauseReason> {
self.last_pause_reason.as_ref()
}
pub fn last_pause_offset(&self) -> u32 {
self.last_pause_offset
}
pub fn last_pause_line(&self) -> u32 {
self.breakpoints
.get(&self.last_pause_offset)
.map(|bp| bp.line)
.unwrap_or(0)
}
pub fn check_pause_at(&mut self, offset: u32) -> Option<StatorError> {
if self.skip_next {
self.skip_next = false;
return None;
}
let reason = if let Some(bp) = self.breakpoints.get(&offset) {
PauseReason::Breakpoint(bp.id)
} else {
let depth = call_stack_depth();
match self.step_mode {
StepMode::None => return None,
StepMode::Into => PauseReason::Step,
StepMode::Over { depth: saved } if depth <= saved => PauseReason::Step,
StepMode::Out { depth: saved } if depth < saved => PauseReason::Step,
_ => return None,
}
};
self.record_pause(reason, offset);
Some(StatorError::DebuggerPaused {
bytecode_offset: offset,
})
}
pub fn on_debugger_statement(&mut self, offset: u32) -> StatorError {
self.record_pause(PauseReason::DebuggerStatement, offset);
StatorError::DebuggerPaused {
bytecode_offset: offset,
}
}
pub fn on_exception(&mut self, offset: u32) -> StatorError {
self.skip_next = true;
self.exception_resume = true;
self.record_pause(PauseReason::Exception, offset);
StatorError::DebuggerPaused {
bytecode_offset: offset,
}
}
pub fn consume_exception_resume(&mut self) -> bool {
let v = self.exception_resume;
self.exception_resume = false;
v
}
pub fn apply_action(&mut self, action: DebugAction) {
if matches!(
self.last_pause_reason,
Some(PauseReason::Breakpoint(_) | PauseReason::Step)
) {
self.skip_next = true;
}
let depth = call_stack_depth();
self.step_mode = match action {
DebugAction::Continue => StepMode::None,
DebugAction::StepInto => StepMode::Into,
DebugAction::StepOver => StepMode::Over { depth },
DebugAction::StepOut => StepMode::Out { depth },
};
}
fn record_pause(&mut self, reason: PauseReason, offset: u32) {
self.last_pause_reason = Some(reason);
self.last_pause_offset = offset;
self.step_mode = StepMode::None;
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use crate::bytecode::bytecode_array::{BytecodeArray, SourcePosition};
use crate::bytecode::bytecodes::{Instruction, Opcode, Operand, encode};
use crate::bytecode::feedback::FeedbackMetadata;
use crate::error::StatorError;
use crate::interpreter::{Interpreter, InterpreterFrame, attach_debugger, detach_debugger};
use crate::objects::value::JsValue;
use super::*;
fn make_bytecodes_with_positions(instructions: Vec<Instruction>) -> BytecodeArray {
let bytes = encode(&instructions);
let mut positions = vec![];
let mut offset: u32 = 0;
for (i, instr) in instructions.iter().enumerate() {
let size = 1 + instr.operand_count() as u32; positions.push(SourcePosition::new(offset, (i + 1) as u32, 1));
offset += size;
}
BytecodeArray::new(
bytes,
vec![],
1, 0, positions,
FeedbackMetadata::empty(),
vec![],
)
}
#[test]
fn test_set_and_remove_breakpoint() {
let mut dbg = Debugger::new();
let id = dbg.set_breakpoint_at_offset(4, 1, 1);
assert_eq!(id, 1);
assert!(
dbg.breakpoints()
.any(|b| b.id == id && b.bytecode_offset == 4)
);
assert!(dbg.remove_breakpoint(id));
assert!(!dbg.breakpoints().any(|b| b.id == id));
assert!(!dbg.remove_breakpoint(id));
}
#[test]
fn test_set_breakpoint_at_line() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaZero, vec![]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(1)]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let mut dbg = Debugger::new();
let id = dbg.set_breakpoint_at_line(&ba, 2);
assert!(id.is_some());
}
#[test]
fn test_set_breakpoint_at_line_not_found() {
let instructions = vec![Instruction::new_unchecked(Opcode::LdaZero, vec![])];
let ba = make_bytecodes_with_positions(instructions);
let mut dbg = Debugger::new();
assert!(dbg.set_breakpoint_at_line(&ba, 99).is_none());
}
#[test]
fn test_breakpoint_locations() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaZero, vec![]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(5)]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let locs = Debugger::breakpoint_locations(&ba);
assert_eq!(locs.len(), 3);
assert_eq!(locs[0].line, 1);
assert_eq!(locs[1].line, 2);
assert_eq!(locs[2].line, 3);
}
fn run_collecting_pauses(
ba: BytecodeArray,
dbg: Rc<RefCell<Debugger>>,
max_pauses: usize,
) -> Vec<PauseReason> {
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
let mut reasons = vec![];
attach_debugger(Rc::clone(&dbg));
for _ in 0..=max_pauses {
match Interpreter::run(&mut frame) {
Ok(_) => break,
Err(StatorError::DebuggerPaused { .. }) => {
let reason = dbg.borrow().last_pause_reason().cloned().unwrap();
reasons.push(reason);
dbg.borrow_mut().apply_action(DebugAction::Continue);
}
Err(e) => panic!("unexpected error: {e:?}"),
}
}
detach_debugger();
reasons
}
#[test]
fn test_breakpoint_hit_once() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaZero, vec![]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(7)]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let dbg = Rc::new(RefCell::new(Debugger::new()));
let bp_id = dbg.borrow_mut().set_breakpoint_at_offset(1, 2, 1);
let reasons = run_collecting_pauses(ba, Rc::clone(&dbg), 5);
assert_eq!(reasons.len(), 1);
assert_eq!(reasons[0], PauseReason::Breakpoint(bp_id));
}
#[test]
fn test_breakpoint_inspect_accumulator_before_instruction() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(10)]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(20)]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let dbg = Rc::new(RefCell::new(Debugger::new()));
dbg.borrow_mut().set_breakpoint_at_offset(2, 2, 1);
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
attach_debugger(Rc::clone(&dbg));
let r = Interpreter::run(&mut frame);
assert!(matches!(r, Err(StatorError::DebuggerPaused { .. })));
assert_eq!(frame.accumulator, JsValue::Smi(10));
dbg.borrow_mut().apply_action(DebugAction::Continue);
let final_val = Interpreter::run(&mut frame).unwrap();
detach_debugger();
assert_eq!(final_val, JsValue::Smi(20));
}
#[test]
fn test_step_into_pauses_each_instruction() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(1)]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(2)]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(3)]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let dbg = Rc::new(RefCell::new(Debugger::new()));
dbg.borrow_mut().set_breakpoint_at_offset(0, 1, 1);
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
let mut pause_count = 0;
attach_debugger(Rc::clone(&dbg));
loop {
match Interpreter::run(&mut frame) {
Ok(_) => break,
Err(StatorError::DebuggerPaused { .. }) => {
pause_count += 1;
dbg.borrow_mut().apply_action(DebugAction::StepInto);
}
Err(e) => panic!("unexpected error: {e:?}"),
}
if pause_count > 10 {
panic!("too many pauses");
}
}
detach_debugger();
assert_eq!(pause_count, 4);
}
#[test]
fn test_step_over_pauses_at_next_instruction_in_same_frame() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(1)]),
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(2)]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let dbg = Rc::new(RefCell::new(Debugger::new()));
dbg.borrow_mut().set_breakpoint_at_offset(0, 1, 1);
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
let mut pauses = vec![];
attach_debugger(Rc::clone(&dbg));
loop {
match Interpreter::run(&mut frame) {
Ok(_) => break,
Err(StatorError::DebuggerPaused { bytecode_offset }) => {
pauses.push(bytecode_offset);
dbg.borrow_mut().apply_action(DebugAction::StepOver);
}
Err(e) => panic!("{e:?}"),
}
if pauses.len() > 10 {
panic!("too many pauses");
}
}
detach_debugger();
assert_eq!(pauses, vec![0, 2, 4]);
}
#[test]
fn test_debugger_statement_pauses_when_hook_attached() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(42)]),
Instruction::new_unchecked(Opcode::Debugger, vec![]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let ba = make_bytecodes_with_positions(instructions);
let dbg = Rc::new(RefCell::new(Debugger::new()));
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
attach_debugger(Rc::clone(&dbg));
let r = Interpreter::run(&mut frame);
assert!(
matches!(r, Err(StatorError::DebuggerPaused { .. })),
"expected debugger pause, got {r:?}"
);
assert_eq!(
dbg.borrow().last_pause_reason(),
Some(&PauseReason::DebuggerStatement)
);
assert_eq!(frame.accumulator, JsValue::Smi(42));
dbg.borrow_mut().apply_action(DebugAction::Continue);
let final_val = Interpreter::run(&mut frame).unwrap();
detach_debugger();
assert_eq!(final_val, JsValue::Smi(42));
}
#[test]
fn test_debugger_statement_noop_without_hook() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(99)]),
Instruction::new_unchecked(Opcode::Debugger, vec![]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let bytes = encode(&instructions);
let ba = BytecodeArray::new(
bytes,
vec![],
0,
0,
vec![],
FeedbackMetadata::empty(),
vec![],
);
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
let result = Interpreter::run(&mut frame).unwrap();
assert_eq!(result, JsValue::Smi(99));
}
#[test]
fn test_pause_on_exceptions_fires_before_throw() {
let instructions = vec![
Instruction::new_unchecked(Opcode::LdaSmi, vec![Operand::Immediate(1)]),
Instruction::new_unchecked(Opcode::Throw, vec![]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let bytes = encode(&instructions);
let ba = BytecodeArray::new(
bytes,
vec![],
1,
0,
vec![],
FeedbackMetadata::empty(),
vec![],
);
let dbg = Rc::new(RefCell::new(Debugger::new()));
dbg.borrow_mut().set_pause_on_exceptions(true);
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
attach_debugger(Rc::clone(&dbg));
let r = Interpreter::run(&mut frame);
assert!(matches!(r, Err(StatorError::DebuggerPaused { .. })));
assert_eq!(
dbg.borrow().last_pause_reason(),
Some(&PauseReason::Exception)
);
assert_eq!(frame.accumulator, JsValue::Smi(1));
dbg.borrow_mut().apply_action(DebugAction::Continue);
let r2 = Interpreter::run(&mut frame);
detach_debugger();
assert!(matches!(r2, Err(StatorError::JsException(_))));
}
#[test]
fn test_evaluate_in_paused_context() {
use crate::bytecode::bytecode_generator::BytecodeGenerator;
use crate::parser;
let source = "debugger; 42;";
let parsed = parser::parse(source).expect("parse");
let ba = BytecodeGenerator::compile_program(&parsed).expect("compile");
let dbg = Rc::new(RefCell::new(Debugger::new()));
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![]);
attach_debugger(Rc::clone(&dbg));
let r = Interpreter::run(&mut frame);
assert!(
matches!(r, Err(StatorError::DebuggerPaused { .. })),
"expected pause, got {r:?}"
);
assert_eq!(
dbg.borrow().last_pause_reason(),
Some(&PauseReason::DebuggerStatement)
);
detach_debugger();
let eval_source = "1 + 2";
let eval_parsed = parser::parse(eval_source).expect("parse eval");
let eval_ba = BytecodeGenerator::compile_program(&eval_parsed).expect("compile eval");
let mut eval_frame = InterpreterFrame::new_with_globals(
Rc::new(eval_ba),
vec![],
Rc::clone(&frame.global_env),
);
let eval_result = Interpreter::run(&mut eval_frame).expect("eval");
assert_eq!(eval_result, JsValue::Smi(3));
attach_debugger(Rc::clone(&dbg));
dbg.borrow_mut().apply_action(DebugAction::Continue);
let final_val = Interpreter::run(&mut frame).ok();
detach_debugger();
assert_eq!(final_val, Some(JsValue::Smi(42)));
}
#[test]
fn test_breakpoint_locations_empty_source_positions() {
let bytes = encode(&[Instruction::new_unchecked(Opcode::Return, vec![])]);
let ba = BytecodeArray::new(
bytes,
vec![],
0,
0,
vec![],
FeedbackMetadata::empty(),
vec![],
);
let locs = Debugger::breakpoint_locations(&ba);
assert!(locs.is_empty());
}
#[test]
fn test_breakpoint_locations_match_source_positions() {
let bytes = encode(&[
Instruction::new_unchecked(Opcode::LdaZero, vec![]),
Instruction::new_unchecked(Opcode::Return, vec![]),
]);
let positions = vec![SourcePosition::new(0, 1, 1), SourcePosition::new(1, 2, 1)];
let ba = BytecodeArray::new(
bytes,
vec![],
0,
0,
positions,
FeedbackMetadata::empty(),
vec![],
);
let locs = Debugger::breakpoint_locations(&ba);
assert_eq!(locs.len(), 2);
assert_eq!(
locs[0],
BreakpointLocation {
bytecode_offset: 0,
line: 1,
column: 1
}
);
assert_eq!(
locs[1],
BreakpointLocation {
bytecode_offset: 1,
line: 2,
column: 1
}
);
}
}