From 7163a7331a3feca1bf12a5f1cb552ca4799027d6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 1 Feb 2022 22:30:05 +0800 Subject: [PATCH] Add commands and status to debugging interface. --- src/api/events.rs | 1 + src/bin/rhai-dbg.rs | 178 +++++++++++++++++++++++----------------- src/eval/chaining.rs | 78 ++++++++++++------ src/eval/debugger.rs | 187 ++++++++++++++++++++++++++++--------------- src/eval/expr.rs | 18 +++-- src/eval/mod.rs | 5 +- src/eval/stmt.rs | 7 +- src/func/call.rs | 17 +++- src/func/script.rs | 39 ++++++--- src/lib.rs | 2 +- tests/debugging.rs | 2 +- 11 files changed, 351 insertions(+), 183 deletions(-) diff --git a/src/api/events.rs b/src/api/events.rs index 4077b506..53de16f5 100644 --- a/src/api/events.rs +++ b/src/api/events.rs @@ -269,6 +269,7 @@ impl Engine { init: impl Fn() -> Dynamic + SendSync + 'static, callback: impl Fn( &mut EvalContext, + crate::eval::DebuggerEvent, crate::ast::ASTNode, Option<&str>, Position, diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs index df668bfc..c90881cf 100644 --- a/src/bin/rhai-dbg.rs +++ b/src/bin/rhai-dbg.rs @@ -1,4 +1,4 @@ -use rhai::debugger::DebuggerCommand; +use rhai::debugger::{BreakPoint, DebuggerCommand, DebuggerEvent}; use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope}; use std::{ @@ -36,6 +36,32 @@ fn print_source(lines: &[String], pos: Position, offset: usize) { } } +fn print_current_source( + context: &mut rhai::EvalContext, + source: Option<&str>, + pos: Position, + lines: &Vec, +) { + let current_source = &mut *context + .global_runtime_state_mut() + .debugger + .state_mut() + .write_lock::() + .unwrap(); + let src = source.unwrap_or(""); + if src != current_source { + println!(">>> Source => {}", source.unwrap_or("main script")); + *current_source = src.into(); + } + if !src.is_empty() { + // Print just a line number for imported modules + println!("{} @ {:?}", src, pos); + } else { + // Print the current source line + print_source(lines, pos, 0); + } +} + /// Pretty-print error. fn print_error(input: &str, mut err: EvalAltResult) { let lines: Vec<_> = input.trim().split('\n').collect(); @@ -71,36 +97,38 @@ fn print_error(input: &str, mut err: EvalAltResult) { /// Print debug help. fn print_debug_help() { - println!("help => print this help"); - println!("quit, exit, kill => quit"); - println!("scope => print the scope"); - println!("print => print all variables de-duplicated"); - println!("print => print the current value of a variable"); + println!("help, h => print this help"); + println!("quit, q, exit, kill => quit"); + println!("scope => print the scope"); + println!("print, p => print all variables de-duplicated"); + println!("print/p => print the current value of a variable"); #[cfg(not(feature = "no_module"))] - println!("imports => print all imported modules"); - println!("node => print the current AST node"); - println!("backtrace => print the current call-stack"); - println!("breakpoints => print all break-points"); - println!("enable => enable a break-point"); - println!("disable => disable a break-point"); - println!("delete => delete a break-point"); - println!("clear => delete all break-points"); + println!("imports => print all imported modules"); + println!("node => print the current AST node"); + println!("list, l => print the current source line"); + println!("backtrace, bt => print the current call-stack"); + println!("info break, i b => print all break-points"); + println!("enable/en => enable a break-point"); + println!("disable/dis => disable a break-point"); + println!("delete, d => delete all break-points"); + println!("delete/d => delete a break-point"); #[cfg(not(feature = "no_position"))] - println!("break => set a new break-point at the current position"); + println!("break, b => set a new break-point at the current position"); #[cfg(not(feature = "no_position"))] - println!("break => set a new break-point at a line number"); + println!("break/b => set a new break-point at a line number"); #[cfg(not(feature = "no_object"))] - println!("break . => set a new break-point for a property access"); - println!("break => set a new break-point for a function call"); + println!("break/b . => set a new break-point for a property access"); + println!("break/b => set a new break-point for a function call"); println!( - "break <#args> => set a new break-point for a function call with #args arguments" + "break/b <#args> => set a new break-point for a function call with #args arguments" ); - println!("throw [message] => throw an exception (message optional)"); - println!("run => restart the script evaluation from beginning"); - println!("step => go to the next expression, diving into functions"); - println!("over => go to the next expression, skipping oer functions"); - println!("next => go to the next statement, skipping over functions"); - println!("continue => continue normal execution"); + println!("throw [message] => throw an exception (message optional)"); + println!("run, r => restart the script evaluation from beginning"); + println!("step, s => go to the next expression, diving into functions"); + println!("over => go to the next expression, skipping oer functions"); + println!("next, n, => go to the next statement, skipping over functions"); + println!("finish, f => continue until the end of the current function call"); + println!("continue, c => continue normal execution"); println!(); } @@ -224,32 +252,34 @@ fn main() { // Store the current source in the debugger state || "".into(), // Main debugging interface - move |context, node, source, pos| { - { - let current_source = &mut *context - .global_runtime_state_mut() - .debugger - .state_mut() - .write_lock::() - .unwrap(); - - let src = source.unwrap_or(""); - - // Check source - if src != current_source { - println!(">>> Source => {}", source.unwrap_or("main script")); - *current_source = src.into(); + move |context, event, node, source, pos| { + match event { + DebuggerEvent::Step => (), + DebuggerEvent::BreakPoint(n) => { + match context.global_runtime_state().debugger.break_points()[n] { + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { .. } => (), + BreakPoint::AtFunctionName { ref name, .. } + | BreakPoint::AtFunctionCall { ref name, .. } => { + println!("! Call to function {}.", name) + } + #[cfg(not(feature = "no_object"))] + BreakPoint::AtProperty { ref name, .. } => { + println!("! Property {} accessed.", name) + } + } } - - if !src.is_empty() { - // Print just a line number for imported modules - println!("{} @ {:?}", src, pos); - } else { - // Print the current source line - print_source(&lines, pos, 0); + DebuggerEvent::FunctionExitWithValue(r) => { + println!("! Return from function call = {}", r) + } + DebuggerEvent::FunctionExitWithError(err) => { + println!("! Return from function call with error: {}", err) } } + // Print current source line + print_current_source(context, source, pos, &lines); + // Read stdin for commands let mut input = String::new(); @@ -267,8 +297,8 @@ fn main() { .collect::>() .as_slice() { - ["help", ..] => print_debug_help(), - ["exit", ..] | ["quit", ..] | ["kill", ..] => { + ["help" | "h", ..] => print_debug_help(), + ["exit" | "quit" | "q" | "kill", ..] => { println!("Script terminated. Bye!"); exit(0); } @@ -276,12 +306,14 @@ fn main() { println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos); println!(); } - ["continue", ..] => break Ok(DebuggerCommand::Continue), - [] | ["step", ..] => break Ok(DebuggerCommand::StepInto), + ["list" | "l", ..] => print_current_source(context, source, pos, &lines), + ["continue" | "c", ..] => break Ok(DebuggerCommand::Continue), + ["finish" | "f", ..] => break Ok(DebuggerCommand::FunctionExit), + [] | ["step" | "s", ..] => break Ok(DebuggerCommand::StepInto), ["over", ..] => break Ok(DebuggerCommand::StepOver), - ["next", ..] => break Ok(DebuggerCommand::Next), + ["next" | "n", ..] => break Ok(DebuggerCommand::Next), ["scope", ..] => print_scope(context.scope(), false), - ["print", var_name, ..] => { + ["print" | "p", var_name, ..] => { if let Some(value) = context.scope().get_value::(var_name) { if value.is::<()>() { println!("=> ()"); @@ -292,7 +324,7 @@ fn main() { eprintln!("Variable not found: {}", var_name); } } - ["print", ..] => print_scope(context.scope(), true), + ["print" | "p"] => print_scope(context.scope(), true), #[cfg(not(feature = "no_module"))] ["imports", ..] => { for (i, (name, module)) in context @@ -311,7 +343,7 @@ fn main() { println!(); } #[cfg(not(feature = "no_function"))] - ["backtrace", ..] => { + ["backtrace" | "bt", ..] => { for frame in context .global_runtime_state() .debugger @@ -322,15 +354,7 @@ fn main() { println!("{}", frame) } } - ["clear", ..] => { - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .clear(); - println!("All break-points cleared."); - } - ["breakpoints", ..] => Iterator::for_each( + ["info", "break", ..] | ["i", "b", ..] => Iterator::for_each( context .global_runtime_state() .debugger @@ -347,7 +371,7 @@ fn main() { _ => println!("[{}] {}", i + 1, bp), }, ), - ["enable", n, ..] => { + ["enable" | "en", n, ..] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() @@ -370,7 +394,7 @@ fn main() { eprintln!("Invalid break-point: '{}'", n); } } - ["disable", n, ..] => { + ["disable" | "dis", n, ..] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() @@ -393,7 +417,7 @@ fn main() { eprintln!("Invalid break-point: '{}'", n); } } - ["delete", n, ..] => { + ["delete" | "d", n, ..] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() @@ -414,7 +438,15 @@ fn main() { eprintln!("Invalid break-point: '{}'", n); } } - ["break", fn_name, args, ..] => { + ["delete" | "d", ..] => { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .clear(); + println!("All break-points deleted."); + } + ["break" | "b", fn_name, args, ..] => { if let Ok(args) = args.parse::() { let bp = rhai::debugger::BreakPoint::AtFunctionCall { name: fn_name.trim().into(), @@ -433,7 +465,7 @@ fn main() { } // Property name #[cfg(not(feature = "no_object"))] - ["break", param] if param.starts_with('.') && param.len() > 1 => { + ["break" | "b", param] if param.starts_with('.') && param.len() > 1 => { let bp = rhai::debugger::BreakPoint::AtProperty { name: param[1..].into(), enabled: true, @@ -447,7 +479,7 @@ fn main() { } // Numeric parameter #[cfg(not(feature = "no_position"))] - ["break", param] if param.parse::().is_ok() => { + ["break" | "b", param] if param.parse::().is_ok() => { let n = param.parse::().unwrap(); let range = if source.is_none() { 1..=lines.len() @@ -472,7 +504,7 @@ fn main() { } } // Function name parameter - ["break", param] => { + ["break" | "b", param] => { let bp = rhai::debugger::BreakPoint::AtFunctionName { name: param.trim().into(), enabled: true, @@ -485,7 +517,7 @@ fn main() { .push(bp); } #[cfg(not(feature = "no_position"))] - ["break", ..] => { + ["break" | "b"] => { let bp = rhai::debugger::BreakPoint::AtPosition { source: source.unwrap_or("").into(), pos, @@ -505,7 +537,7 @@ fn main() { let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); } - ["run", ..] => { + ["run" | "r", ..] => { println!("Restarting script..."); break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); } diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index 53cd1767..43a4e4a8 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -156,7 +156,9 @@ impl Engine { if !_terminate_chaining => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + } let mut idx_val_for_setter = idx_val.clone(); let idx_pos = x.lhs.position(); @@ -204,7 +206,9 @@ impl Engine { // xxx[rhs] op= new_val _ if new_val.is_some() => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + } let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); let mut idx_val_for_setter = idx_val.clone(); @@ -249,7 +253,9 @@ impl Engine { // xxx[rhs] _ => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + } self.get_indexed_mut( global, state, lib, target, idx_val, pos, false, true, level, @@ -268,9 +274,13 @@ impl Engine { let call_args = &mut idx_val.into_fn_call_args(); #[cfg(feature = "debugging")] - let reset_debugger = self.run_debugger_with_reset( - scope, global, state, lib, this_ptr, rhs, level, - )?; + let reset_debugger = if self.debugger.is_some() { + self.run_debugger_with_reset( + scope, global, state, lib, this_ptr, rhs, level, + )? + } else { + None + }; let result = self.make_method_call( global, state, lib, name, *hashes, target, call_args, *pos, level, @@ -292,7 +302,9 @@ impl Engine { // {xxx:map}.id op= ??? Expr::Property(x, pos) if target.is::() && new_val.is_some() => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + } let index = x.2.clone().into(); let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); @@ -312,7 +324,9 @@ impl Engine { // {xxx:map}.id Expr::Property(x, pos) if target.is::() => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + } let index = x.2.clone().into(); let val = self.get_indexed_mut( @@ -323,7 +337,9 @@ impl Engine { // xxx.id op= ??? Expr::Property(x, pos) if new_val.is_some() => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + } let ((getter, hash_get), (setter, hash_set), name) = x.as_ref(); let ((mut new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); @@ -402,7 +418,9 @@ impl Engine { // xxx.id Expr::Property(x, pos) => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + } let ((getter, hash_get), _, name) = x.as_ref(); let hash = crate::ast::FnCallHashes::from_native(*hash_get); @@ -442,9 +460,11 @@ impl Engine { let val_target = &mut match x.lhs { Expr::Property(ref p, pos) => { #[cfg(feature = "debugging")] - self.run_debugger( - scope, global, state, lib, this_ptr, _node, level, - )?; + if self.debugger.is_some() { + self.run_debugger( + scope, global, state, lib, this_ptr, _node, level, + )?; + } let index = p.2.clone().into(); self.get_indexed_mut( @@ -457,9 +477,13 @@ impl Engine { let call_args = &mut idx_val.into_fn_call_args(); #[cfg(feature = "debugging")] - let reset_debugger = self.run_debugger_with_reset( - scope, global, state, lib, this_ptr, _node, level, - )?; + let reset_debugger = if self.debugger.is_some() { + self.run_debugger_with_reset( + scope, global, state, lib, this_ptr, _node, level, + )? + } else { + None + }; let result = self.make_method_call( global, state, lib, name, *hashes, target, call_args, pos, @@ -494,9 +518,11 @@ impl Engine { // xxx.prop[expr] | xxx.prop.expr Expr::Property(ref p, pos) => { #[cfg(feature = "debugging")] - self.run_debugger( - scope, global, state, lib, this_ptr, _node, level, - )?; + if self.debugger.is_some() { + self.run_debugger( + scope, global, state, lib, this_ptr, _node, level, + )?; + } let ((getter, hash_get), (setter, hash_set), name) = p.as_ref(); let rhs_chain = rhs.into(); @@ -597,9 +623,13 @@ impl Engine { let args = &mut idx_val.into_fn_call_args(); #[cfg(feature = "debugging")] - let reset_debugger = self.run_debugger_with_reset( - scope, global, state, lib, this_ptr, _node, level, - )?; + let reset_debugger = if self.debugger.is_some() { + self.run_debugger_with_reset( + scope, global, state, lib, this_ptr, _node, level, + )? + } else { + None + }; let result = self.make_method_call( global, state, lib, name, *hashes, target, args, pos, level, @@ -664,7 +694,9 @@ impl Engine { // id.??? or id[???] Expr::Variable(_, var_pos, x) => { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, lhs, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, lhs, level)?; + } #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, *var_pos)?; diff --git a/src/eval/debugger.rs b/src/eval/debugger.rs index b6c1e487..28c86e4d 100644 --- a/src/eval/debugger.rs +++ b/src/eval/debugger.rs @@ -3,7 +3,7 @@ use super::{EvalContext, EvalState, GlobalRuntimeState}; use crate::ast::{ASTNode, Expr, Stmt}; -use crate::{Dynamic, Engine, Identifier, Module, Position, RhaiResultOf, Scope}; +use crate::{Dynamic, Engine, EvalAltResult, Identifier, Module, Position, RhaiResultOf, Scope}; use std::fmt; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -17,11 +17,22 @@ pub type OnDebuggingInit = dyn Fn() -> Dynamic + Send + Sync; /// Callback function for debugging. #[cfg(not(feature = "sync"))] -pub type OnDebuggerCallback = - dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf; +pub type OnDebuggerCallback = dyn Fn( + &mut EvalContext, + DebuggerEvent, + ASTNode, + Option<&str>, + Position, +) -> RhaiResultOf; /// Callback function for debugging. #[cfg(feature = "sync")] -pub type OnDebuggerCallback = dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf +pub type OnDebuggerCallback = dyn Fn( + &mut EvalContext, + DebuggerEvent, + ASTNode, + Option<&str>, + Position, + ) -> RhaiResultOf + Send + Sync; @@ -36,6 +47,30 @@ pub enum DebuggerCommand { StepOver, // Run to the next statement, skipping over functions. Next, + // Run to the end of the current function call. + FunctionExit, +} + +/// The debugger status. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum DebuggerStatus { + // Stop at the next statement or expression. + Next(bool, bool), + // Run to the end of the current function call. + FunctionExit(usize), +} + +/// A event that triggers the debugger. +#[derive(Debug, Clone, Copy)] +pub enum DebuggerEvent<'a> { + // Break on next step. + Step, + // Break on break-point. + BreakPoint(usize), + // Return from a function with a value. + FunctionExitWithValue(&'a Dynamic), + // Return from a function with a value. + FunctionExitWithError(&'a EvalAltResult), } /// A break-point for debugging. @@ -198,7 +233,7 @@ impl fmt::Display for CallStackFrame { #[derive(Debug, Clone, Hash)] pub struct Debugger { /// The current status command. - status: DebuggerCommand, + status: DebuggerStatus, /// The current state. state: Dynamic, /// The current set of break-points. @@ -215,9 +250,9 @@ impl Debugger { pub fn new(engine: &Engine) -> Self { Self { status: if engine.debugger.is_some() { - DebuggerCommand::StepInto + DebuggerStatus::Next(true, true) } else { - DebuggerCommand::Continue + DebuggerStatus::Next(false, false) }, state: if let Some((ref init, _)) = engine.debugger { init() @@ -280,31 +315,26 @@ impl Debugger { /// Get the current status of this [`Debugger`]. #[inline(always)] #[must_use] - pub fn status(&self) -> DebuggerCommand { + pub(crate) fn status(&self) -> DebuggerStatus { self.status } - /// Get a mutable reference to the current status of this [`Debugger`]. - #[inline(always)] - #[must_use] - pub fn status_mut(&mut self) -> &mut DebuggerCommand { - &mut self.status - } /// Set the status of this [`Debugger`]. #[inline(always)] - pub fn reset_status(&mut self, status: Option) { + pub(crate) fn reset_status(&mut self, status: Option) { if let Some(cmd) = status { self.status = cmd; } } - /// Does a particular [`AST` Node][ASTNode] trigger a break-point? + /// Returns the first break-point triggered by a particular [`AST` Node][ASTNode]. #[must_use] - pub fn is_break_point(&self, src: &str, node: ASTNode) -> bool { + pub fn is_break_point(&self, src: &str, node: ASTNode) -> Option { let _src = src; self.break_points() .iter() - .filter(|&bp| bp.is_enabled()) - .any(|bp| match bp { + .enumerate() + .filter(|&(_, bp)| bp.is_enabled()) + .find(|&(_, bp)| match bp { #[cfg(not(feature = "no_position"))] BreakPoint::AtPosition { pos, .. } if pos.is_none() => false, #[cfg(not(feature = "no_position"))] @@ -333,6 +363,7 @@ impl Debugger { _ => false, }, }) + .map(|(i, _)| i) } /// Get a slice of all [`BreakPoint`]'s. #[inline(always)] @@ -371,14 +402,9 @@ impl Engine { } /// Run the debugger callback. /// - /// Returns `true` if the debugger needs to be reactivated at the end of the block, statement or + /// Returns `Some` if the debugger needs to be reactivated at the end of the block, statement or /// function call. /// - /// # Note - /// - /// When the debugger callback return [`DebuggerCommand::StepOver`], the debugger if temporarily - /// disabled and `true` is returned. - /// /// It is up to the [`Engine`] to reactivate the debugger. #[inline] #[must_use] @@ -391,61 +417,94 @@ impl Engine { this_ptr: &mut Option<&mut Dynamic>, node: impl Into>, level: usize, - ) -> RhaiResultOf> { - if let Some((_, ref on_debugger)) = self.debugger { - let node = node.into(); + ) -> RhaiResultOf> { + let node = node.into(); - // Skip transitive nodes - match node { - ASTNode::Expr(Expr::Stmt(_)) | ASTNode::Stmt(Stmt::Expr(_)) => return Ok(None), - _ => (), - } + // Skip transitive nodes + match node { + ASTNode::Expr(Expr::Stmt(_)) | ASTNode::Stmt(Stmt::Expr(_)) => return Ok(None), + _ => (), + } - let stop = match global.debugger.status { - DebuggerCommand::Continue => false, - DebuggerCommand::Next => matches!(node, ASTNode::Stmt(_)), - DebuggerCommand::StepInto | DebuggerCommand::StepOver => true, - }; + let stop = match global.debugger.status { + DebuggerStatus::Next(false, false) => false, + DebuggerStatus::Next(true, false) => matches!(node, ASTNode::Stmt(_)), + DebuggerStatus::Next(false, true) => matches!(node, ASTNode::Expr(_)), + DebuggerStatus::Next(true, true) => true, + DebuggerStatus::FunctionExit(_) => false, + }; - if !stop && !global.debugger.is_break_point(&global.source, node) { + let event = if stop { + DebuggerEvent::Step + } else { + if let Some(bp) = global.debugger.is_break_point(&global.source, node) { + DebuggerEvent::BreakPoint(bp) + } else { return Ok(None); } + }; - let source = global.source.clone(); - let source = if source.is_empty() { - None - } else { - Some(source.as_str()) - }; + self.run_debugger_raw(scope, global, state, lib, this_ptr, node, event, level) + } + /// Run the debugger callback unconditionally. + /// + /// Returns `Some` if the debugger needs to be reactivated at the end of the block, statement or + /// function call. + /// + /// It is up to the [`Engine`] to reactivate the debugger. + #[inline] + #[must_use] + pub(crate) fn run_debugger_raw<'a>( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + this_ptr: &mut Option<&mut Dynamic>, + node: ASTNode<'a>, + event: DebuggerEvent, + level: usize, + ) -> Result, Box> { + let source = global.source.clone(); + let source = if source.is_empty() { + None + } else { + Some(source.as_str()) + }; - let mut context = crate::EvalContext { - engine: self, - scope, - global, - state, - lib, - this_ptr, - level, - }; + let mut context = crate::EvalContext { + engine: self, + scope, + global, + state, + lib, + this_ptr, + level, + }; - let command = on_debugger(&mut context, node, source, node.position())?; + if let Some((_, ref on_debugger)) = self.debugger { + let command = on_debugger(&mut context, event, node, source, node.position())?; match command { DebuggerCommand::Continue => { - global.debugger.status = DebuggerCommand::Continue; + global.debugger.status = DebuggerStatus::Next(false, false); Ok(None) } DebuggerCommand::Next => { - global.debugger.status = DebuggerCommand::Continue; - Ok(Some(DebuggerCommand::Next)) - } - DebuggerCommand::StepInto => { - global.debugger.status = DebuggerCommand::StepInto; - Ok(None) + global.debugger.status = DebuggerStatus::Next(false, false); + Ok(Some(DebuggerStatus::Next(true, false))) } DebuggerCommand::StepOver => { - global.debugger.status = DebuggerCommand::Continue; - Ok(Some(DebuggerCommand::StepOver)) + global.debugger.status = DebuggerStatus::Next(false, false); + Ok(Some(DebuggerStatus::Next(true, true))) + } + DebuggerCommand::StepInto => { + global.debugger.status = DebuggerStatus::Next(true, true); + Ok(None) + } + DebuggerCommand::FunctionExit => { + global.debugger.status = DebuggerStatus::FunctionExit(context.call_level()); + Ok(None) } } } else { diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 6b8a1287..0b848b9c 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -266,8 +266,11 @@ impl Engine { // binary operators are also function calls. if let Expr::FnCall(x, pos) = expr { #[cfg(feature = "debugging")] - let reset_debugger = - self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?; + let reset_debugger = if self.debugger.is_some() { + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)? + } else { + None + }; #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; @@ -286,7 +289,9 @@ impl Engine { // will cost more than the mis-predicted `match` branch. if let Expr::Variable(index, var_pos, x) = expr { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, expr, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, expr, level)?; + } #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; @@ -303,8 +308,11 @@ impl Engine { } #[cfg(feature = "debugging")] - let reset_debugger = - self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?; + let reset_debugger = if self.debugger.is_some() { + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)? + } else { + None + }; #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 0609c9b8..7550c3ab 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -14,7 +14,10 @@ pub use chaining::{ChainArgument, ChainType}; #[cfg(not(feature = "no_function"))] pub use debugger::CallStackFrame; #[cfg(feature = "debugging")] -pub use debugger::{BreakPoint, Debugger, DebuggerCommand, OnDebuggerCallback, OnDebuggingInit}; +pub use debugger::{ + BreakPoint, Debugger, DebuggerCommand, DebuggerEvent, DebuggerStatus, OnDebuggerCallback, + OnDebuggingInit, +}; pub use eval_context::EvalContext; pub use eval_state::EvalState; #[cfg(not(feature = "no_module"))] diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index a5dd2da4..b853d3dc 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -202,8 +202,11 @@ impl Engine { level: usize, ) -> RhaiResult { #[cfg(feature = "debugging")] - let reset_debugger = - self.run_debugger_with_reset(scope, global, state, lib, this_ptr, stmt, level)?; + let reset_debugger = if self.debugger.is_some() { + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, stmt, level)? + } else { + None + }; // Coded this way for better branch prediction. // Popular branches are lifted out of the `match` statement into their own branches. diff --git a/src/func/call.rs b/src/func/call.rs index 95ea4758..612982e4 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -905,11 +905,15 @@ impl Engine { Ok(( if let Expr::Stack(slot, _) = arg_expr { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; + } constants[*slot].clone() } else if let Some(value) = arg_expr.get_literal_value() { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; + } value } else { self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level)? @@ -1155,7 +1159,9 @@ impl Engine { let first_expr = first_arg.unwrap(); #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, first_expr, level)?; + if self.debugger.is_some() { + self.run_debugger(scope, global, state, lib, this_ptr, first_expr, level)?; + } // func(x, ...) -> x.func(...) a_expr.iter().try_for_each(|expr| { @@ -1239,7 +1245,10 @@ impl Engine { // &mut first argument and avoid cloning the value if !args_expr.is_empty() && args_expr[0].is_variable_access(true) { #[cfg(feature = "debugging")] - self.run_debugger(scope, global, state, lib, this_ptr, &args_expr[0], level)?; + if self.debugger.is_some() { + let node = &args_expr[0]; + self.run_debugger(scope, global, state, lib, this_ptr, node, level)?; + } // func(x, ...) -> x.func(...) arg_values.push(Dynamic::UNIT); diff --git a/src/func/script.rs b/src/func/script.rs index e42a28fd..650f9db2 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -65,16 +65,16 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, pos)?; - if fn_def.body.is_empty() { - return Ok(Dynamic::UNIT); - } - // Check for stack overflow #[cfg(not(feature = "unchecked"))] if level > self.max_call_levels() { return Err(ERR::ErrorStackOverflow(pos).into()); } + if fn_def.body.is_empty() { + return Ok(Dynamic::UNIT); + } + let orig_scope_len = scope.len(); #[cfg(not(feature = "no_module"))] let orig_imports_len = global.num_imports(); @@ -133,7 +133,7 @@ impl Engine { }; // Evaluate the function - let result = self + let mut result = self .eval_stmt_block( scope, global, @@ -166,6 +166,31 @@ impl Engine { _ => make_error(fn_def.name.to_string(), fn_def, global, err, pos), }); + #[cfg(feature = "debugging")] + { + if self.debugger.is_some() { + match global.debugger.status() { + crate::eval::DebuggerStatus::FunctionExit(n) if n >= level => { + let node = crate::ast::Stmt::Noop(pos); + let node = (&node).into(); + let event = match result { + Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r), + Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err), + }; + if let Err(err) = self.run_debugger_raw( + scope, global, state, lib, this_ptr, node, event, level, + ) { + result = Err(err); + } + } + _ => (), + } + } + + // Pop the call stack + global.debugger.rewind_call_stack(orig_call_stack_len); + } + // Remove all local variables and imported modules if rewind_scope { scope.rewind(orig_scope_len); @@ -185,10 +210,6 @@ impl Engine { // Restore state state.rewind_fn_resolution_caches(orig_fn_resolution_caches_len); - // Pop the call stack - #[cfg(feature = "debugging")] - global.debugger.rewind_call_stack(orig_call_stack_len); - result } diff --git a/src/lib.rs b/src/lib.rs index 6d19cde0..a94f146e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,7 +164,7 @@ pub use types::{ pub mod debugger { #[cfg(not(feature = "no_function"))] pub use super::eval::CallStackFrame; - pub use super::eval::{BreakPoint, Debugger, DebuggerCommand}; + pub use super::eval::{BreakPoint, Debugger, DebuggerCommand, DebuggerEvent}; } /// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most diff --git a/tests/debugging.rs b/tests/debugging.rs index d0ed9cfe..036bd4fb 100644 --- a/tests/debugging.rs +++ b/tests/debugging.rs @@ -50,7 +50,7 @@ fn test_debugger_state() -> Result<(), Box> { state.insert("foo".into(), false.into()); Dynamic::from_map(state) }, - |context, _, _, _| { + |context, _, _, _, _| { // Get global runtime state let global = context.global_runtime_state_mut();