Add ability for debugger to throw errors.

This commit is contained in:
Stephen Chung 2022-01-25 21:31:35 +08:00
parent b4f679d35f
commit 3c2ac7f0c5
8 changed files with 78 additions and 50 deletions

View File

@ -272,7 +272,7 @@ impl Engine {
crate::ast::ASTNode,
Option<&str>,
Position,
) -> crate::eval::DebuggerCommand
) -> RhaiResultOf<crate::eval::DebuggerCommand>
+ SendSync
+ 'static,
) -> &mut Self {

View File

@ -90,8 +90,11 @@ fn print_debug_help() {
println!(
"break <func> <#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!("next => go to the next statement but don't dive 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!();
}
@ -212,9 +215,9 @@ fn main() {
input.clear();
match stdin().read_line(&mut input) {
Ok(0) => break DebuggerCommand::Continue,
Ok(0) => break Ok(DebuggerCommand::Continue),
Ok(_) => match input
.trim_end()
.trim()
.split_whitespace()
.collect::<Vec<_>>()
.as_slice()
@ -228,9 +231,10 @@ fn main() {
println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos);
println!();
}
["continue", ..] => break DebuggerCommand::Continue,
[] | ["step", ..] => break DebuggerCommand::StepInto,
["next", ..] => break DebuggerCommand::StepOver,
["continue", ..] => break Ok(DebuggerCommand::Continue),
[] | ["step", ..] => break Ok(DebuggerCommand::StepInto),
["over", ..] => break Ok(DebuggerCommand::StepOver),
["next", ..] => break Ok(DebuggerCommand::Next),
["scope", ..] => print_scope(context.scope()),
#[cfg(not(feature = "no_function"))]
["backtrace", ..] => {
@ -406,6 +410,15 @@ fn main() {
.break_points_mut()
.push(bp);
}
["throw"] => break Err(EvalAltResult::ErrorRuntime(Dynamic::UNIT, pos).into()),
["throw", _msg, ..] => {
let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or("");
break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into());
}
["run", ..] => {
println!("Restarting script...");
break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into());
}
[cmd, ..] => eprintln!("Invalid debugger command: '{}'", cmd),
},
Err(err) => panic!("input error: {}", err),
@ -422,14 +435,19 @@ fn main() {
engine.set_module_resolver(resolver);
}
// Create scope
let mut scope = Scope::new();
print_debug_help();
// Evaluate
if let Err(err) = engine.run_ast_with_scope(&mut scope, &main_ast) {
while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &main_ast) {
match *err {
// Loop back to restart
EvalAltResult::ErrorTerminated(_, _) => (),
// Break evaluation
_ => {
print_error(&script, *err);
break;
}
}
}
}

View File

@ -156,7 +156,7 @@ impl Engine {
if !_terminate_chaining =>
{
#[cfg(feature = "debugging")]
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 idx_pos = x.lhs.position();
@ -204,7 +204,7 @@ 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);
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 +249,7 @@ impl Engine {
// xxx[rhs]
_ => {
#[cfg(feature = "debugging")]
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(
global, state, lib, target, idx_val, pos, false, true, level,
@ -270,7 +270,7 @@ impl Engine {
#[cfg(feature = "debugging")]
let reset_debugger = self.run_debugger_with_reset(
scope, global, state, lib, this_ptr, rhs, level,
);
)?;
let result = self.make_method_call(
global, state, lib, name, *hashes, target, call_args, *pos, level,
@ -292,7 +292,7 @@ impl Engine {
// {xxx:map}.id op= ???
Expr::Property(x) if target.is::<crate::Map>() && new_val.is_some() => {
#[cfg(feature = "debugging")]
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level);
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?;
let (name, pos) = &x.2;
let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`");
@ -313,7 +313,7 @@ impl Engine {
// {xxx:map}.id
Expr::Property(x) if target.is::<crate::Map>() => {
#[cfg(feature = "debugging")]
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level);
self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?;
let (name, pos) = &x.2;
let index = name.into();
@ -325,7 +325,7 @@ impl Engine {
// xxx.id op= ???
Expr::Property(x) if new_val.is_some() => {
#[cfg(feature = "debugging")]
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, pos)) = x.as_ref();
let ((mut new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`");
@ -404,7 +404,7 @@ impl Engine {
// xxx.id
Expr::Property(x) => {
#[cfg(feature = "debugging")]
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, pos)) = x.as_ref();
let hash = crate::ast::FnCallHashes::from_native(*hash_get);
@ -446,7 +446,7 @@ impl Engine {
#[cfg(feature = "debugging")]
self.run_debugger(
scope, global, state, lib, this_ptr, _node, level,
);
)?;
let (name, pos) = &p.2;
let index = name.into();
@ -462,7 +462,7 @@ impl Engine {
#[cfg(feature = "debugging")]
let reset_debugger = self.run_debugger_with_reset(
scope, global, state, lib, this_ptr, _node, level,
);
)?;
let result = self.make_method_call(
global, state, lib, name, *hashes, target, call_args, pos,
@ -499,7 +499,7 @@ impl Engine {
#[cfg(feature = "debugging")]
self.run_debugger(
scope, global, state, lib, this_ptr, _node, level,
);
)?;
let ((getter, hash_get), (setter, hash_set), (name, pos)) =
p.as_ref();
@ -603,7 +603,7 @@ impl Engine {
#[cfg(feature = "debugging")]
let reset_debugger = self.run_debugger_with_reset(
scope, global, state, lib, this_ptr, _node, level,
);
)?;
let result = self.make_method_call(
global, state, lib, name, *hashes, target, args, pos, level,
@ -668,7 +668,7 @@ impl Engine {
// id.??? or id[???]
Expr::Variable(_, var_pos, x) => {
#[cfg(feature = "debugging")]
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"))]
self.inc_operations(&mut global.num_operations, *var_pos)?;

View File

@ -3,19 +3,21 @@
use super::{EvalContext, EvalState, GlobalRuntimeState};
use crate::ast::{ASTNode, Expr, Stmt};
use crate::{Dynamic, Engine, Identifier, Module, Position, Scope};
use crate::{Dynamic, Engine, Identifier, Module, Position, RhaiResultOf, Scope};
use std::fmt;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
/// A standard callback function for debugging.
#[cfg(not(feature = "sync"))]
pub type OnDebuggerCallback =
Box<dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> DebuggerCommand + 'static>;
pub type OnDebuggerCallback = Box<
dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand>
+ 'static,
>;
/// A standard callback function for debugging.
#[cfg(feature = "sync")]
pub type OnDebuggerCallback = Box<
dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> DebuggerCommand
dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand>
+ Send
+ Sync
+ 'static,
@ -28,8 +30,10 @@ pub enum DebuggerCommand {
Continue,
// Step into the next expression, diving into functions.
StepInto,
// Run to the next statement, stepping over functions.
// Run to the next expression or statement, stepping over functions.
StepOver,
// Run to the next statement, skipping over functions.
Next,
}
/// A break-point for debugging.
@ -318,12 +322,14 @@ impl Engine {
this_ptr: &mut Option<&mut Dynamic>,
node: impl Into<ASTNode<'a>>,
level: usize,
) {
) -> RhaiResultOf<()> {
if let Some(cmd) =
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, node, level)
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, node, level)?
{
global.debugger.set_status(cmd);
}
Ok(())
}
/// Run the debugger callback.
///
@ -347,18 +353,18 @@ impl Engine {
this_ptr: &mut Option<&mut Dynamic>,
node: impl Into<ASTNode<'a>>,
level: usize,
) -> Option<DebuggerCommand> {
) -> RhaiResultOf<Option<DebuggerCommand>> {
if let Some(ref on_debugger) = self.debugger {
let node = node.into();
let stop = match global.debugger.status {
DebuggerCommand::Continue => false,
DebuggerCommand::StepOver => matches!(node, ASTNode::Stmt(_)),
DebuggerCommand::StepInto => true,
DebuggerCommand::Next => matches!(node, ASTNode::Stmt(_)),
DebuggerCommand::StepInto | DebuggerCommand::StepOver => true,
};
if !stop && !global.debugger.is_break_point(&global.source, node) {
return None;
return Ok(None);
}
let source = global.source.clone();
@ -378,24 +384,28 @@ impl Engine {
level,
};
let command = on_debugger(&mut context, node, source, node.position());
let command = on_debugger(&mut context, node, source, node.position())?;
match command {
DebuggerCommand::Continue => {
global.debugger.set_status(DebuggerCommand::Continue);
None
Ok(None)
}
DebuggerCommand::Next => {
global.debugger.set_status(DebuggerCommand::Continue);
Ok(Some(DebuggerCommand::Next))
}
DebuggerCommand::StepInto => {
global.debugger.set_status(DebuggerCommand::StepInto);
None
Ok(None)
}
DebuggerCommand::StepOver => {
global.debugger.set_status(DebuggerCommand::Continue);
Some(DebuggerCommand::StepOver)
Ok(Some(DebuggerCommand::StepOver))
}
}
} else {
None
Ok(None)
}
}
}

View File

@ -267,7 +267,7 @@ impl Engine {
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);
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?;
#[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, expr.position())?;
@ -286,7 +286,7 @@ 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);
self.run_debugger(scope, global, state, lib, this_ptr, expr, level)?;
#[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, expr.position())?;
@ -304,7 +304,7 @@ impl Engine {
#[cfg(feature = "debugging")]
let reset_debugger =
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)?;
#[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, expr.position())?;

View File

@ -196,7 +196,7 @@ impl Engine {
) -> RhaiResult {
#[cfg(feature = "debugging")]
let reset_debugger =
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)?;
// Coded this way for better branch prediction.
// Popular branches are lifted out of the `match` statement into their own branches.

View File

@ -890,11 +890,11 @@ 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);
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);
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)?
@ -1140,7 +1140,7 @@ impl Engine {
let first_expr = first_arg.unwrap();
#[cfg(feature = "debugging")]
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(...)
a_expr.iter().try_for_each(|expr| {
@ -1223,7 +1223,7 @@ 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);
self.run_debugger(scope, global, state, lib, this_ptr, &args_expr[0], level)?;
// func(x, ...) -> x.func(...)
arg_values.push(Dynamic::UNIT);

View File

@ -62,7 +62,7 @@ mod debugging_functions {
if !pos.is_none() {
map.insert("line".into(), (pos.line().unwrap() as INT).into());
map.insert(
"pos".into(),
"position".into(),
(pos.position().unwrap_or(0) as INT).into(),
);
}