Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/instruction-stepping.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ When in instruction stepping mode, the following commands are available:
- `c`, `continue` - Continue execution until completion
- `q`, `quit`, `exit` - Exit instruction stepping mode

### Loop Guard (Repeated Pause Detection)

If the debugger repeatedly pauses at the exact same instruction without making any forward progress (e.g., hitting the same breakpoint infinitely), it will automatically intercept the loop after a threshold (5 repetitions) and warn you.
You will receive an actionable error suggestion, and if the repeated pauses are intentional, you can simply issue your step or continue command again to proceed for another cycle.

### Example Session

```
Expand Down
29 changes: 29 additions & 0 deletions src/debugger/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,16 @@ impl DebuggerEngine {
self.source_map.as_ref()?.lookup(wasm_offset)
}

fn check_repeated_pause(&mut self) -> Result<()> {
if self.stepper.pause_repeat_count() >= 5 {
self.stepper.reset_pause_count();
return Err(miette::miette!(
"Repeated identical pause state detected (no progress). If this is intentional, you may issue the continue or step command again."
));
}
Ok(())
}

/// Enable instruction-level debugging.
pub fn enable_instruction_debug(&mut self, wasm_bytes: &[u8]) -> Result<()> {
self.try_load_source_map(wasm_bytes);
Expand Down Expand Up @@ -387,6 +397,7 @@ impl DebuggerEngine {
if let Ok(state) = self.state.lock() {
state.call_stack().display();
}
self.check_repeated_pause()?;
}

result
Expand Down Expand Up @@ -536,6 +547,9 @@ impl DebuggerEngine {
state.set_pause_reason(PauseReason::EndOfExecution);
}
}
if stepped {
self.check_repeated_pause()?;
}
Ok(stepped)
}

Expand All @@ -558,6 +572,9 @@ impl DebuggerEngine {
state.set_pause_reason(PauseReason::EndOfExecution);
}
}
if stepped {
self.check_repeated_pause()?;
}
Ok(stepped)
}

Expand All @@ -580,6 +597,9 @@ impl DebuggerEngine {
state.set_pause_reason(PauseReason::EndOfExecution);
}
}
if stepped {
self.check_repeated_pause()?;
}
Ok(stepped)
}

Expand Down Expand Up @@ -608,6 +628,9 @@ impl DebuggerEngine {
state.set_pause_reason(PauseReason::EndOfExecution);
}
}
if paused {
self.check_repeated_pause()?;
}
Ok(StepOverResult { paused, location })
}

Expand All @@ -630,6 +653,9 @@ impl DebuggerEngine {
state.set_pause_reason(PauseReason::EndOfExecution);
}
}
if stepped {
self.check_repeated_pause()?;
}
Ok(stepped)
}

Expand All @@ -652,6 +678,9 @@ impl DebuggerEngine {
state.set_pause_reason(PauseReason::EndOfExecution);
}
}
if stepped {
self.check_repeated_pause()?;
}
Ok(stepped)
}

Expand Down
63 changes: 58 additions & 5 deletions src/debugger/stepper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub struct Stepper {
active: bool,
step_mode: StepMode,
pause_next: bool,
last_pause_triple: Option<(usize, usize, StepMode)>,
pause_repeat_count: usize,
}

impl Stepper {
Expand All @@ -17,6 +19,8 @@ impl Stepper {
active: false,
step_mode: StepMode::StepInto,
pause_next: false,
last_pause_triple: None,
pause_repeat_count: 0,
}
}

Expand All @@ -41,13 +45,42 @@ impl Stepper {
self.step_mode
}

pub fn pause_repeat_count(&self) -> usize {
self.pause_repeat_count
}

pub fn reset_pause_count(&mut self) {
self.pause_repeat_count = 0;
}

fn track_pause(&mut self, debug_state: &DebugState) {
if let Some(inst) = debug_state.current_instruction() {
let current_triple = (
inst.offset,
debug_state.instruction_pointer().call_stack_depth(),
self.step_mode,
);

if self.last_pause_triple == Some(current_triple) {
self.pause_repeat_count += 1;
} else {
self.last_pause_triple = Some(current_triple);
self.pause_repeat_count = 0;
}
}
}

pub fn step_into(&mut self, debug_state: &mut DebugState) -> bool {
if !self.active {
return false;
}
self.step_mode = StepMode::StepInto;
debug_state.start_instruction_stepping(StepMode::StepInto);
debug_state.next_instruction().is_some()
let stepped = debug_state.next_instruction().is_some();
if stepped {
self.track_pause(debug_state);
}
stepped
}

pub fn step_over(&mut self, debug_state: &mut DebugState) -> bool {
Expand All @@ -56,7 +89,11 @@ impl Stepper {
}
self.step_mode = StepMode::StepOver;
debug_state.start_instruction_stepping(StepMode::StepOver);
self.advance_to_depth(debug_state, false)
let stepped = self.advance_to_depth(debug_state, false);
if stepped {
self.track_pause(debug_state);
}
stepped
}

/// Step over to the next distinct source line within the same call frame.
Expand Down Expand Up @@ -96,6 +133,7 @@ impl Stepper {
};

if is_different_line {
self.track_pause(debug_state);
return true;
}
}
Expand All @@ -109,7 +147,11 @@ impl Stepper {
}
self.step_mode = StepMode::StepOut;
debug_state.start_instruction_stepping(StepMode::StepOut);
self.advance_to_depth(debug_state, true)
let stepped = self.advance_to_depth(debug_state, true);
if stepped {
self.track_pause(debug_state);
}
stepped
}

pub fn step_block(&mut self, debug_state: &mut DebugState) -> bool {
Expand All @@ -118,14 +160,22 @@ impl Stepper {
}
self.step_mode = StepMode::StepBlock;
debug_state.start_instruction_stepping(StepMode::StepBlock);
self.find_next_control_flow(debug_state)
let stepped = self.find_next_control_flow(debug_state);
if stepped {
self.track_pause(debug_state);
}
stepped
}

pub fn step_back(&mut self, debug_state: &mut DebugState) -> bool {
if !self.active {
return false;
}
debug_state.previous_instruction().is_some()
let stepped = debug_state.previous_instruction().is_some();
if stepped {
self.track_pause(debug_state);
}
stepped
}

pub fn continue_execution(&mut self, debug_state: &mut DebugState) {
Expand Down Expand Up @@ -155,6 +205,7 @@ impl Stepper {
return false;
}
if self.should_pause(instruction, debug_state) {
self.track_pause(debug_state);
self.pause_next = false;
return true;
}
Expand All @@ -164,6 +215,8 @@ impl Stepper {
pub fn reset(&mut self) {
self.active = false;
self.pause_next = false;
self.last_pause_triple = None;
self.pause_repeat_count = 0;
}

fn advance_to_depth(&self, debug_state: &mut DebugState, strictly_lower: bool) -> bool {
Expand Down
Loading