diff --git a/docs/instruction-stepping.md b/docs/instruction-stepping.md index e6991ae9..ef5e53b5 100644 --- a/docs/instruction-stepping.md +++ b/docs/instruction-stepping.md @@ -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 ``` diff --git a/src/debugger/engine.rs b/src/debugger/engine.rs index f3d2ac34..95cefc88 100644 --- a/src/debugger/engine.rs +++ b/src/debugger/engine.rs @@ -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); @@ -387,6 +397,7 @@ impl DebuggerEngine { if let Ok(state) = self.state.lock() { state.call_stack().display(); } + self.check_repeated_pause()?; } result @@ -536,6 +547,9 @@ impl DebuggerEngine { state.set_pause_reason(PauseReason::EndOfExecution); } } + if stepped { + self.check_repeated_pause()?; + } Ok(stepped) } @@ -558,6 +572,9 @@ impl DebuggerEngine { state.set_pause_reason(PauseReason::EndOfExecution); } } + if stepped { + self.check_repeated_pause()?; + } Ok(stepped) } @@ -580,6 +597,9 @@ impl DebuggerEngine { state.set_pause_reason(PauseReason::EndOfExecution); } } + if stepped { + self.check_repeated_pause()?; + } Ok(stepped) } @@ -608,6 +628,9 @@ impl DebuggerEngine { state.set_pause_reason(PauseReason::EndOfExecution); } } + if paused { + self.check_repeated_pause()?; + } Ok(StepOverResult { paused, location }) } @@ -630,6 +653,9 @@ impl DebuggerEngine { state.set_pause_reason(PauseReason::EndOfExecution); } } + if stepped { + self.check_repeated_pause()?; + } Ok(stepped) } @@ -652,6 +678,9 @@ impl DebuggerEngine { state.set_pause_reason(PauseReason::EndOfExecution); } } + if stepped { + self.check_repeated_pause()?; + } Ok(stepped) } diff --git a/src/debugger/stepper.rs b/src/debugger/stepper.rs index 237596f9..ff361568 100644 --- a/src/debugger/stepper.rs +++ b/src/debugger/stepper.rs @@ -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 { @@ -17,6 +19,8 @@ impl Stepper { active: false, step_mode: StepMode::StepInto, pause_next: false, + last_pause_triple: None, + pause_repeat_count: 0, } } @@ -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 { @@ -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. @@ -96,6 +133,7 @@ impl Stepper { }; if is_different_line { + self.track_pause(debug_state); return true; } } @@ -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 { @@ -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) { @@ -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; } @@ -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 {