Add commands and status to debugging interface.

This commit is contained in:
Stephen Chung 2022-02-01 22:30:05 +08:00
parent dca0185323
commit 7163a7331a
11 changed files with 351 additions and 183 deletions

View File

@ -269,6 +269,7 @@ impl Engine {
init: impl Fn() -> Dynamic + SendSync + 'static, init: impl Fn() -> Dynamic + SendSync + 'static,
callback: impl Fn( callback: impl Fn(
&mut EvalContext, &mut EvalContext,
crate::eval::DebuggerEvent,
crate::ast::ASTNode, crate::ast::ASTNode,
Option<&str>, Option<&str>,
Position, Position,

View File

@ -1,4 +1,4 @@
use rhai::debugger::DebuggerCommand; use rhai::debugger::{BreakPoint, DebuggerCommand, DebuggerEvent};
use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope}; use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope};
use std::{ 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<String>,
) {
let current_source = &mut *context
.global_runtime_state_mut()
.debugger
.state_mut()
.write_lock::<ImmutableString>()
.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. /// Pretty-print error.
fn print_error(input: &str, mut err: EvalAltResult) { fn print_error(input: &str, mut err: EvalAltResult) {
let lines: Vec<_> = input.trim().split('\n').collect(); let lines: Vec<_> = input.trim().split('\n').collect();
@ -71,36 +97,38 @@ fn print_error(input: &str, mut err: EvalAltResult) {
/// Print debug help. /// Print debug help.
fn print_debug_help() { fn print_debug_help() {
println!("help => print this help"); println!("help, h => print this help");
println!("quit, exit, kill => quit"); println!("quit, q, exit, kill => quit");
println!("scope => print the scope"); println!("scope => print the scope");
println!("print => print all variables de-duplicated"); println!("print, p => print all variables de-duplicated");
println!("print <variable> => print the current value of a variable"); println!("print/p <variable> => print the current value of a variable");
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
println!("imports => print all imported modules"); println!("imports => print all imported modules");
println!("node => print the current AST node"); println!("node => print the current AST node");
println!("backtrace => print the current call-stack"); println!("list, l => print the current source line");
println!("breakpoints => print all break-points"); println!("backtrace, bt => print the current call-stack");
println!("enable <bp#> => enable a break-point"); println!("info break, i b => print all break-points");
println!("disable <bp#> => disable a break-point"); println!("enable/en <bp#> => enable a break-point");
println!("delete <bp#> => delete a break-point"); println!("disable/dis <bp#> => disable a break-point");
println!("clear => delete all break-points"); println!("delete, d => delete all break-points");
println!("delete/d <bp#> => delete a break-point");
#[cfg(not(feature = "no_position"))] #[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"))] #[cfg(not(feature = "no_position"))]
println!("break <line#> => set a new break-point at a line number"); println!("break/b <line#> => set a new break-point at a line number");
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
println!("break .<prop> => set a new break-point for a property access"); println!("break/b .<prop> => set a new break-point for a property access");
println!("break <func> => set a new break-point for a function call"); println!("break/b <func> => set a new break-point for a function call");
println!( println!(
"break <func> <#args> => set a new break-point for a function call with #args arguments" "break/b <func> <#args> => set a new break-point for a function call with #args arguments"
); );
println!("throw [message] => throw an exception (message optional)"); println!("throw [message] => throw an exception (message optional)");
println!("run => restart the script evaluation from beginning"); println!("run, r => restart the script evaluation from beginning");
println!("step => go to the next expression, diving into functions"); println!("step, s => go to the next expression, diving into functions");
println!("over => go to the next expression, skipping oer functions"); println!("over => go to the next expression, skipping oer functions");
println!("next => go to the next statement, skipping over functions"); println!("next, n, <Enter> => go to the next statement, skipping over functions");
println!("continue => continue normal execution"); println!("finish, f => continue until the end of the current function call");
println!("continue, c => continue normal execution");
println!(); println!();
} }
@ -224,31 +252,33 @@ fn main() {
// Store the current source in the debugger state // Store the current source in the debugger state
|| "".into(), || "".into(),
// Main debugging interface // Main debugging interface
move |context, node, source, pos| { move |context, event, node, source, pos| {
{ match event {
let current_source = &mut *context DebuggerEvent::Step => (),
.global_runtime_state_mut() DebuggerEvent::BreakPoint(n) => {
.debugger match context.global_runtime_state().debugger.break_points()[n] {
.state_mut() #[cfg(not(feature = "no_position"))]
.write_lock::<ImmutableString>() BreakPoint::AtPosition { .. } => (),
.unwrap(); BreakPoint::AtFunctionName { ref name, .. }
| BreakPoint::AtFunctionCall { ref name, .. } => {
let src = source.unwrap_or(""); println!("! Call to function {}.", name)
// Check source
if src != current_source {
println!(">>> Source => {}", source.unwrap_or("main script"));
*current_source = src.into();
} }
#[cfg(not(feature = "no_object"))]
if !src.is_empty() { BreakPoint::AtProperty { ref name, .. } => {
// Print just a line number for imported modules println!("! Property {} accessed.", name)
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 // Read stdin for commands
let mut input = String::new(); let mut input = String::new();
@ -267,8 +297,8 @@ fn main() {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.as_slice() .as_slice()
{ {
["help", ..] => print_debug_help(), ["help" | "h", ..] => print_debug_help(),
["exit", ..] | ["quit", ..] | ["kill", ..] => { ["exit" | "quit" | "q" | "kill", ..] => {
println!("Script terminated. Bye!"); println!("Script terminated. Bye!");
exit(0); exit(0);
} }
@ -276,12 +306,14 @@ fn main() {
println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos); println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos);
println!(); println!();
} }
["continue", ..] => break Ok(DebuggerCommand::Continue), ["list" | "l", ..] => print_current_source(context, source, pos, &lines),
[] | ["step", ..] => break Ok(DebuggerCommand::StepInto), ["continue" | "c", ..] => break Ok(DebuggerCommand::Continue),
["finish" | "f", ..] => break Ok(DebuggerCommand::FunctionExit),
[] | ["step" | "s", ..] => break Ok(DebuggerCommand::StepInto),
["over", ..] => break Ok(DebuggerCommand::StepOver), ["over", ..] => break Ok(DebuggerCommand::StepOver),
["next", ..] => break Ok(DebuggerCommand::Next), ["next" | "n", ..] => break Ok(DebuggerCommand::Next),
["scope", ..] => print_scope(context.scope(), false), ["scope", ..] => print_scope(context.scope(), false),
["print", var_name, ..] => { ["print" | "p", var_name, ..] => {
if let Some(value) = context.scope().get_value::<Dynamic>(var_name) { if let Some(value) = context.scope().get_value::<Dynamic>(var_name) {
if value.is::<()>() { if value.is::<()>() {
println!("=> ()"); println!("=> ()");
@ -292,7 +324,7 @@ fn main() {
eprintln!("Variable not found: {}", var_name); eprintln!("Variable not found: {}", var_name);
} }
} }
["print", ..] => print_scope(context.scope(), true), ["print" | "p"] => print_scope(context.scope(), true),
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
["imports", ..] => { ["imports", ..] => {
for (i, (name, module)) in context for (i, (name, module)) in context
@ -311,7 +343,7 @@ fn main() {
println!(); println!();
} }
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
["backtrace", ..] => { ["backtrace" | "bt", ..] => {
for frame in context for frame in context
.global_runtime_state() .global_runtime_state()
.debugger .debugger
@ -322,15 +354,7 @@ fn main() {
println!("{}", frame) println!("{}", frame)
} }
} }
["clear", ..] => { ["info", "break", ..] | ["i", "b", ..] => Iterator::for_each(
context
.global_runtime_state_mut()
.debugger
.break_points_mut()
.clear();
println!("All break-points cleared.");
}
["breakpoints", ..] => Iterator::for_each(
context context
.global_runtime_state() .global_runtime_state()
.debugger .debugger
@ -347,7 +371,7 @@ fn main() {
_ => println!("[{}] {}", i + 1, bp), _ => println!("[{}] {}", i + 1, bp),
}, },
), ),
["enable", n, ..] => { ["enable" | "en", n, ..] => {
if let Ok(n) = n.parse::<usize>() { if let Ok(n) = n.parse::<usize>() {
let range = 1..=context let range = 1..=context
.global_runtime_state_mut() .global_runtime_state_mut()
@ -370,7 +394,7 @@ fn main() {
eprintln!("Invalid break-point: '{}'", n); eprintln!("Invalid break-point: '{}'", n);
} }
} }
["disable", n, ..] => { ["disable" | "dis", n, ..] => {
if let Ok(n) = n.parse::<usize>() { if let Ok(n) = n.parse::<usize>() {
let range = 1..=context let range = 1..=context
.global_runtime_state_mut() .global_runtime_state_mut()
@ -393,7 +417,7 @@ fn main() {
eprintln!("Invalid break-point: '{}'", n); eprintln!("Invalid break-point: '{}'", n);
} }
} }
["delete", n, ..] => { ["delete" | "d", n, ..] => {
if let Ok(n) = n.parse::<usize>() { if let Ok(n) = n.parse::<usize>() {
let range = 1..=context let range = 1..=context
.global_runtime_state_mut() .global_runtime_state_mut()
@ -414,7 +438,15 @@ fn main() {
eprintln!("Invalid break-point: '{}'", n); 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::<usize>() { if let Ok(args) = args.parse::<usize>() {
let bp = rhai::debugger::BreakPoint::AtFunctionCall { let bp = rhai::debugger::BreakPoint::AtFunctionCall {
name: fn_name.trim().into(), name: fn_name.trim().into(),
@ -433,7 +465,7 @@ fn main() {
} }
// Property name // Property name
#[cfg(not(feature = "no_object"))] #[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 { let bp = rhai::debugger::BreakPoint::AtProperty {
name: param[1..].into(), name: param[1..].into(),
enabled: true, enabled: true,
@ -447,7 +479,7 @@ fn main() {
} }
// Numeric parameter // Numeric parameter
#[cfg(not(feature = "no_position"))] #[cfg(not(feature = "no_position"))]
["break", param] if param.parse::<usize>().is_ok() => { ["break" | "b", param] if param.parse::<usize>().is_ok() => {
let n = param.parse::<usize>().unwrap(); let n = param.parse::<usize>().unwrap();
let range = if source.is_none() { let range = if source.is_none() {
1..=lines.len() 1..=lines.len()
@ -472,7 +504,7 @@ fn main() {
} }
} }
// Function name parameter // Function name parameter
["break", param] => { ["break" | "b", param] => {
let bp = rhai::debugger::BreakPoint::AtFunctionName { let bp = rhai::debugger::BreakPoint::AtFunctionName {
name: param.trim().into(), name: param.trim().into(),
enabled: true, enabled: true,
@ -485,7 +517,7 @@ fn main() {
.push(bp); .push(bp);
} }
#[cfg(not(feature = "no_position"))] #[cfg(not(feature = "no_position"))]
["break", ..] => { ["break" | "b"] => {
let bp = rhai::debugger::BreakPoint::AtPosition { let bp = rhai::debugger::BreakPoint::AtPosition {
source: source.unwrap_or("").into(), source: source.unwrap_or("").into(),
pos, pos,
@ -505,7 +537,7 @@ fn main() {
let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or("");
break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into());
} }
["run", ..] => { ["run" | "r", ..] => {
println!("Restarting script..."); println!("Restarting script...");
break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into());
} }

View File

@ -156,7 +156,9 @@ impl Engine {
if !_terminate_chaining => if !_terminate_chaining =>
{ {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?;
}
let mut idx_val_for_setter = idx_val.clone(); let mut idx_val_for_setter = idx_val.clone();
let idx_pos = x.lhs.position(); let idx_pos = x.lhs.position();
@ -204,7 +206,9 @@ impl Engine {
// xxx[rhs] op= new_val // xxx[rhs] op= new_val
_ if new_val.is_some() => { _ if new_val.is_some() => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; 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 ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`");
let mut idx_val_for_setter = idx_val.clone(); let mut idx_val_for_setter = idx_val.clone();
@ -249,7 +253,9 @@ impl Engine {
// xxx[rhs] // xxx[rhs]
_ => { _ => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?;
}
self.get_indexed_mut( self.get_indexed_mut(
global, state, lib, target, idx_val, pos, false, true, level, 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(); let call_args = &mut idx_val.into_fn_call_args();
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
let reset_debugger = self.run_debugger_with_reset( let reset_debugger = if self.debugger.is_some() {
self.run_debugger_with_reset(
scope, global, state, lib, this_ptr, rhs, level, scope, global, state, lib, this_ptr, rhs, level,
)?; )?
} else {
None
};
let result = self.make_method_call( let result = self.make_method_call(
global, state, lib, name, *hashes, target, call_args, *pos, level, global, state, lib, name, *hashes, target, call_args, *pos, level,
@ -292,7 +302,9 @@ impl Engine {
// {xxx:map}.id op= ??? // {xxx:map}.id op= ???
Expr::Property(x, pos) if target.is::<crate::Map>() && new_val.is_some() => { Expr::Property(x, pos) if target.is::<crate::Map>() && new_val.is_some() => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?;
}
let index = x.2.clone().into(); let index = x.2.clone().into();
let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`");
@ -312,7 +324,9 @@ impl Engine {
// {xxx:map}.id // {xxx:map}.id
Expr::Property(x, pos) if target.is::<crate::Map>() => { Expr::Property(x, pos) if target.is::<crate::Map>() => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?;
}
let index = x.2.clone().into(); let index = x.2.clone().into();
let val = self.get_indexed_mut( let val = self.get_indexed_mut(
@ -323,7 +337,9 @@ impl Engine {
// xxx.id op= ??? // xxx.id op= ???
Expr::Property(x, pos) if new_val.is_some() => { Expr::Property(x, pos) if new_val.is_some() => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?;
}
let ((getter, hash_get), (setter, hash_set), name) = x.as_ref(); 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`"); let ((mut new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`");
@ -402,7 +418,9 @@ impl Engine {
// xxx.id // xxx.id
Expr::Property(x, pos) => { Expr::Property(x, pos) => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?;
}
let ((getter, hash_get), _, name) = x.as_ref(); let ((getter, hash_get), _, name) = x.as_ref();
let hash = crate::ast::FnCallHashes::from_native(*hash_get); let hash = crate::ast::FnCallHashes::from_native(*hash_get);
@ -442,9 +460,11 @@ impl Engine {
let val_target = &mut match x.lhs { let val_target = &mut match x.lhs {
Expr::Property(ref p, pos) => { Expr::Property(ref p, pos) => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger( self.run_debugger(
scope, global, state, lib, this_ptr, _node, level, scope, global, state, lib, this_ptr, _node, level,
)?; )?;
}
let index = p.2.clone().into(); let index = p.2.clone().into();
self.get_indexed_mut( self.get_indexed_mut(
@ -457,9 +477,13 @@ impl Engine {
let call_args = &mut idx_val.into_fn_call_args(); let call_args = &mut idx_val.into_fn_call_args();
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
let reset_debugger = self.run_debugger_with_reset( let reset_debugger = if self.debugger.is_some() {
self.run_debugger_with_reset(
scope, global, state, lib, this_ptr, _node, level, scope, global, state, lib, this_ptr, _node, level,
)?; )?
} else {
None
};
let result = self.make_method_call( let result = self.make_method_call(
global, state, lib, name, *hashes, target, call_args, pos, global, state, lib, name, *hashes, target, call_args, pos,
@ -494,9 +518,11 @@ impl Engine {
// xxx.prop[expr] | xxx.prop.expr // xxx.prop[expr] | xxx.prop.expr
Expr::Property(ref p, pos) => { Expr::Property(ref p, pos) => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger( self.run_debugger(
scope, global, state, lib, this_ptr, _node, level, scope, global, state, lib, this_ptr, _node, level,
)?; )?;
}
let ((getter, hash_get), (setter, hash_set), name) = p.as_ref(); let ((getter, hash_get), (setter, hash_set), name) = p.as_ref();
let rhs_chain = rhs.into(); let rhs_chain = rhs.into();
@ -597,9 +623,13 @@ impl Engine {
let args = &mut idx_val.into_fn_call_args(); let args = &mut idx_val.into_fn_call_args();
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
let reset_debugger = self.run_debugger_with_reset( let reset_debugger = if self.debugger.is_some() {
self.run_debugger_with_reset(
scope, global, state, lib, this_ptr, _node, level, scope, global, state, lib, this_ptr, _node, level,
)?; )?
} else {
None
};
let result = self.make_method_call( let result = self.make_method_call(
global, state, lib, name, *hashes, target, args, pos, level, global, state, lib, name, *hashes, target, args, pos, level,
@ -664,7 +694,9 @@ impl Engine {
// id.??? or id[???] // id.??? or id[???]
Expr::Variable(_, var_pos, x) => { Expr::Variable(_, var_pos, x) => {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, lhs, level)?; self.run_debugger(scope, global, state, lib, this_ptr, lhs, level)?;
}
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, *var_pos)?; self.inc_operations(&mut global.num_operations, *var_pos)?;

View File

@ -3,7 +3,7 @@
use super::{EvalContext, EvalState, GlobalRuntimeState}; use super::{EvalContext, EvalState, GlobalRuntimeState};
use crate::ast::{ASTNode, Expr, Stmt}; 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; use std::fmt;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -17,11 +17,22 @@ pub type OnDebuggingInit = dyn Fn() -> Dynamic + Send + Sync;
/// Callback function for debugging. /// Callback function for debugging.
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
pub type OnDebuggerCallback = pub type OnDebuggerCallback = dyn Fn(
dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand>; &mut EvalContext,
DebuggerEvent,
ASTNode,
Option<&str>,
Position,
) -> RhaiResultOf<DebuggerCommand>;
/// Callback function for debugging. /// Callback function for debugging.
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
pub type OnDebuggerCallback = dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand> pub type OnDebuggerCallback = dyn Fn(
&mut EvalContext,
DebuggerEvent,
ASTNode,
Option<&str>,
Position,
) -> RhaiResultOf<DebuggerCommand>
+ Send + Send
+ Sync; + Sync;
@ -36,6 +47,30 @@ pub enum DebuggerCommand {
StepOver, StepOver,
// Run to the next statement, skipping over functions. // Run to the next statement, skipping over functions.
Next, 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. /// A break-point for debugging.
@ -198,7 +233,7 @@ impl fmt::Display for CallStackFrame {
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct Debugger { pub struct Debugger {
/// The current status command. /// The current status command.
status: DebuggerCommand, status: DebuggerStatus,
/// The current state. /// The current state.
state: Dynamic, state: Dynamic,
/// The current set of break-points. /// The current set of break-points.
@ -215,9 +250,9 @@ impl Debugger {
pub fn new(engine: &Engine) -> Self { pub fn new(engine: &Engine) -> Self {
Self { Self {
status: if engine.debugger.is_some() { status: if engine.debugger.is_some() {
DebuggerCommand::StepInto DebuggerStatus::Next(true, true)
} else { } else {
DebuggerCommand::Continue DebuggerStatus::Next(false, false)
}, },
state: if let Some((ref init, _)) = engine.debugger { state: if let Some((ref init, _)) = engine.debugger {
init() init()
@ -280,31 +315,26 @@ impl Debugger {
/// Get the current status of this [`Debugger`]. /// Get the current status of this [`Debugger`].
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]
pub fn status(&self) -> DebuggerCommand { pub(crate) fn status(&self) -> DebuggerStatus {
self.status 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`]. /// Set the status of this [`Debugger`].
#[inline(always)] #[inline(always)]
pub fn reset_status(&mut self, status: Option<DebuggerCommand>) { pub(crate) fn reset_status(&mut self, status: Option<DebuggerStatus>) {
if let Some(cmd) = status { if let Some(cmd) = status {
self.status = cmd; 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] #[must_use]
pub fn is_break_point(&self, src: &str, node: ASTNode) -> bool { pub fn is_break_point(&self, src: &str, node: ASTNode) -> Option<usize> {
let _src = src; let _src = src;
self.break_points() self.break_points()
.iter() .iter()
.filter(|&bp| bp.is_enabled()) .enumerate()
.any(|bp| match bp { .filter(|&(_, bp)| bp.is_enabled())
.find(|&(_, bp)| match bp {
#[cfg(not(feature = "no_position"))] #[cfg(not(feature = "no_position"))]
BreakPoint::AtPosition { pos, .. } if pos.is_none() => false, BreakPoint::AtPosition { pos, .. } if pos.is_none() => false,
#[cfg(not(feature = "no_position"))] #[cfg(not(feature = "no_position"))]
@ -333,6 +363,7 @@ impl Debugger {
_ => false, _ => false,
}, },
}) })
.map(|(i, _)| i)
} }
/// Get a slice of all [`BreakPoint`]'s. /// Get a slice of all [`BreakPoint`]'s.
#[inline(always)] #[inline(always)]
@ -371,14 +402,9 @@ impl Engine {
} }
/// Run the debugger callback. /// 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. /// 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. /// It is up to the [`Engine`] to reactivate the debugger.
#[inline] #[inline]
#[must_use] #[must_use]
@ -391,8 +417,7 @@ impl Engine {
this_ptr: &mut Option<&mut Dynamic>, this_ptr: &mut Option<&mut Dynamic>,
node: impl Into<ASTNode<'a>>, node: impl Into<ASTNode<'a>>,
level: usize, level: usize,
) -> RhaiResultOf<Option<DebuggerCommand>> { ) -> RhaiResultOf<Option<DebuggerStatus>> {
if let Some((_, ref on_debugger)) = self.debugger {
let node = node.into(); let node = node.into();
// Skip transitive nodes // Skip transitive nodes
@ -402,15 +427,44 @@ impl Engine {
} }
let stop = match global.debugger.status { let stop = match global.debugger.status {
DebuggerCommand::Continue => false, DebuggerStatus::Next(false, false) => false,
DebuggerCommand::Next => matches!(node, ASTNode::Stmt(_)), DebuggerStatus::Next(true, false) => matches!(node, ASTNode::Stmt(_)),
DebuggerCommand::StepInto | DebuggerCommand::StepOver => true, 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); return Ok(None);
} }
};
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<Option<DebuggerStatus>, Box<crate::EvalAltResult>> {
let source = global.source.clone(); let source = global.source.clone();
let source = if source.is_empty() { let source = if source.is_empty() {
None None
@ -428,24 +482,29 @@ impl Engine {
level, 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 { match command {
DebuggerCommand::Continue => { DebuggerCommand::Continue => {
global.debugger.status = DebuggerCommand::Continue; global.debugger.status = DebuggerStatus::Next(false, false);
Ok(None) Ok(None)
} }
DebuggerCommand::Next => { DebuggerCommand::Next => {
global.debugger.status = DebuggerCommand::Continue; global.debugger.status = DebuggerStatus::Next(false, false);
Ok(Some(DebuggerCommand::Next)) Ok(Some(DebuggerStatus::Next(true, false)))
}
DebuggerCommand::StepInto => {
global.debugger.status = DebuggerCommand::StepInto;
Ok(None)
} }
DebuggerCommand::StepOver => { DebuggerCommand::StepOver => {
global.debugger.status = DebuggerCommand::Continue; global.debugger.status = DebuggerStatus::Next(false, false);
Ok(Some(DebuggerCommand::StepOver)) 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 { } else {

View File

@ -266,8 +266,11 @@ impl Engine {
// binary operators are also function calls. // binary operators are also function calls.
if let Expr::FnCall(x, pos) = expr { if let Expr::FnCall(x, pos) = expr {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
let reset_debugger = let reset_debugger = if self.debugger.is_some() {
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?; self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?
} else {
None
};
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, expr.position())?; self.inc_operations(&mut global.num_operations, expr.position())?;
@ -286,7 +289,9 @@ impl Engine {
// will cost more than the mis-predicted `match` branch. // will cost more than the mis-predicted `match` branch.
if let Expr::Variable(index, var_pos, x) = expr { if let Expr::Variable(index, var_pos, x) = expr {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, expr, level)?; self.run_debugger(scope, global, state, lib, this_ptr, expr, level)?;
}
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, expr.position())?; self.inc_operations(&mut global.num_operations, expr.position())?;
@ -303,8 +308,11 @@ impl Engine {
} }
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
let reset_debugger = let reset_debugger = if self.debugger.is_some() {
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?; self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?
} else {
None
};
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, expr.position())?; self.inc_operations(&mut global.num_operations, expr.position())?;

View File

@ -14,7 +14,10 @@ pub use chaining::{ChainArgument, ChainType};
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub use debugger::CallStackFrame; pub use debugger::CallStackFrame;
#[cfg(feature = "debugging")] #[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_context::EvalContext;
pub use eval_state::EvalState; pub use eval_state::EvalState;
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]

View File

@ -202,8 +202,11 @@ impl Engine {
level: usize, level: usize,
) -> RhaiResult { ) -> RhaiResult {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
let reset_debugger = let reset_debugger = if self.debugger.is_some() {
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, stmt, level)?; self.run_debugger_with_reset(scope, global, state, lib, this_ptr, stmt, level)?
} else {
None
};
// Coded this way for better branch prediction. // Coded this way for better branch prediction.
// Popular branches are lifted out of the `match` statement into their own branches. // Popular branches are lifted out of the `match` statement into their own branches.

View File

@ -905,11 +905,15 @@ impl Engine {
Ok(( Ok((
if let Expr::Stack(slot, _) = arg_expr { if let Expr::Stack(slot, _) = arg_expr {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?;
}
constants[*slot].clone() constants[*slot].clone()
} else if let Some(value) = arg_expr.get_literal_value() { } else if let Some(value) = arg_expr.get_literal_value() {
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?;
}
value value
} else { } else {
self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level)? self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level)?
@ -1155,7 +1159,9 @@ impl Engine {
let first_expr = first_arg.unwrap(); let first_expr = first_arg.unwrap();
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
if self.debugger.is_some() {
self.run_debugger(scope, global, state, lib, this_ptr, first_expr, level)?; self.run_debugger(scope, global, state, lib, this_ptr, first_expr, level)?;
}
// func(x, ...) -> x.func(...) // func(x, ...) -> x.func(...)
a_expr.iter().try_for_each(|expr| { a_expr.iter().try_for_each(|expr| {
@ -1239,7 +1245,10 @@ impl Engine {
// &mut first argument and avoid cloning the value // &mut first argument and avoid cloning the value
if !args_expr.is_empty() && args_expr[0].is_variable_access(true) { if !args_expr.is_empty() && args_expr[0].is_variable_access(true) {
#[cfg(feature = "debugging")] #[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(...) // func(x, ...) -> x.func(...)
arg_values.push(Dynamic::UNIT); arg_values.push(Dynamic::UNIT);

View File

@ -65,16 +65,16 @@ impl Engine {
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, pos)?; self.inc_operations(&mut global.num_operations, pos)?;
if fn_def.body.is_empty() {
return Ok(Dynamic::UNIT);
}
// Check for stack overflow // Check for stack overflow
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
if level > self.max_call_levels() { if level > self.max_call_levels() {
return Err(ERR::ErrorStackOverflow(pos).into()); return Err(ERR::ErrorStackOverflow(pos).into());
} }
if fn_def.body.is_empty() {
return Ok(Dynamic::UNIT);
}
let orig_scope_len = scope.len(); let orig_scope_len = scope.len();
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
let orig_imports_len = global.num_imports(); let orig_imports_len = global.num_imports();
@ -133,7 +133,7 @@ impl Engine {
}; };
// Evaluate the function // Evaluate the function
let result = self let mut result = self
.eval_stmt_block( .eval_stmt_block(
scope, scope,
global, global,
@ -166,6 +166,31 @@ impl Engine {
_ => make_error(fn_def.name.to_string(), fn_def, global, err, pos), _ => 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 // Remove all local variables and imported modules
if rewind_scope { if rewind_scope {
scope.rewind(orig_scope_len); scope.rewind(orig_scope_len);
@ -185,10 +210,6 @@ impl Engine {
// Restore state // Restore state
state.rewind_fn_resolution_caches(orig_fn_resolution_caches_len); 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 result
} }

View File

@ -164,7 +164,7 @@ pub use types::{
pub mod debugger { pub mod debugger {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub use super::eval::CallStackFrame; 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 /// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most

View File

@ -50,7 +50,7 @@ fn test_debugger_state() -> Result<(), Box<EvalAltResult>> {
state.insert("foo".into(), false.into()); state.insert("foo".into(), false.into());
Dynamic::from_map(state) Dynamic::from_map(state)
}, },
|context, _, _, _| { |context, _, _, _, _| {
// Get global runtime state // Get global runtime state
let global = context.global_runtime_state_mut(); let global = context.global_runtime_state_mut();