Support call stack and FunctionExit for native functions.

This commit is contained in:
Stephen Chung 2022-02-02 14:47:35 +08:00
parent 7163a7331a
commit 4a80483749
13 changed files with 190 additions and 77 deletions

View File

@ -24,7 +24,7 @@ New features
* A debugging interface is added. * A debugging interface is added.
* A new bin tool, `rhai-dbg` (aka _The Rhai Debugger_), is added to showcase the debugging interface. * A new bin tool, `rhai-dbg` (aka _The Rhai Debugger_), is added to showcase the debugging interface.
* A new package, `DebuggingPackage`, is added which contains the `stack_trace` function to get the current call stack anywhere in a script. * A new package, `DebuggingPackage`, is added which contains the `back_trace` function to get the current call stack anywhere in a script.
Enhancements Enhancements
------------ ------------
@ -32,6 +32,7 @@ Enhancements
* Default features for dependencies (such as `ahash/std` and `num-traits/std`) are no longer required. * Default features for dependencies (such as `ahash/std` and `num-traits/std`) are no longer required.
* The `no_module` feature now eliminates large sections of code via feature gates. * The `no_module` feature now eliminates large sections of code via feature gates.
* Debug display of `AST` is improved. * Debug display of `AST` is improved.
* `NativeCallContext::call_level()` is added to give the current nesting level of function calls.
REPL tool changes REPL tool changes
----------------- -----------------

View File

@ -270,10 +270,30 @@ fn main() {
} }
} }
DebuggerEvent::FunctionExitWithValue(r) => { DebuggerEvent::FunctionExitWithValue(r) => {
println!("! Return from function call = {}", r) println!(
"! Return from function call '{}' => {}",
context
.global_runtime_state()
.debugger
.call_stack()
.last()
.unwrap()
.fn_name,
r
)
} }
DebuggerEvent::FunctionExitWithError(err) => { DebuggerEvent::FunctionExitWithError(err) => {
println!("! Return from function call with error: {}", err) println!(
"! Return from function call '{}' with error: {}",
context
.global_runtime_state()
.debugger
.call_stack()
.last()
.unwrap()
.fn_name,
err
)
} }
} }

View File

@ -220,6 +220,7 @@ impl Engine {
Ok(ref mut obj_ptr) => { Ok(ref mut obj_ptr) => {
self.eval_op_assignment( self.eval_op_assignment(
global, state, lib, op_info, op_pos, obj_ptr, root, new_val, global, state, lib, op_info, op_pos, obj_ptr, root, new_val,
level,
) )
.map_err(|err| err.fill_position(new_pos))?; .map_err(|err| err.fill_position(new_pos))?;
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
@ -314,6 +315,7 @@ impl Engine {
)?; )?;
self.eval_op_assignment( self.eval_op_assignment(
global, state, lib, op_info, op_pos, val_target, root, new_val, global, state, lib, op_info, op_pos, val_target, root, new_val,
level,
) )
.map_err(|err| err.fill_position(new_pos))?; .map_err(|err| err.fill_position(new_pos))?;
} }
@ -380,6 +382,7 @@ impl Engine {
&mut (&mut orig_val).into(), &mut (&mut orig_val).into(),
root, root,
new_val, new_val,
level,
) )
.map_err(|err| err.fill_position(new_pos))?; .map_err(|err| err.fill_position(new_pos))?;

View File

@ -346,13 +346,15 @@ impl Debugger {
node.position() == *pos && _src == source node.position() == *pos && _src == source
} }
BreakPoint::AtFunctionName { name, .. } => match node { BreakPoint::AtFunctionName { name, .. } => match node {
ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { ASTNode::Expr(Expr::FnCall(x, _))
x.name == *name | ASTNode::Stmt(Stmt::FnCall(x, _))
} | ASTNode::Stmt(Stmt::Expr(Expr::FnCall(x, _))) => x.name == *name,
_ => false, _ => false,
}, },
BreakPoint::AtFunctionCall { name, args, .. } => match node { BreakPoint::AtFunctionCall { name, args, .. } => match node {
ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { ASTNode::Expr(Expr::FnCall(x, _))
| ASTNode::Stmt(Stmt::FnCall(x, _))
| ASTNode::Stmt(Stmt::Expr(Expr::FnCall(x, _))) => {
x.args.len() == *args && x.name == *name x.args.len() == *args && x.name == *name
} }
_ => false, _ => false,
@ -503,7 +505,14 @@ impl Engine {
Ok(None) Ok(None)
} }
DebuggerCommand::FunctionExit => { DebuggerCommand::FunctionExit => {
global.debugger.status = DebuggerStatus::FunctionExit(context.call_level()); // Bump a level if it is a function call
let level = match node {
ASTNode::Expr(Expr::FnCall(_, _))
| ASTNode::Stmt(Stmt::FnCall(_, _))
| ASTNode::Stmt(Stmt::Expr(Expr::FnCall(_, _))) => context.call_level() + 1,
_ => context.call_level(),
};
global.debugger.status = DebuggerStatus::FunctionExit(level);
Ok(None) Ok(None)
} }
} }

View File

@ -353,6 +353,7 @@ impl Engine {
&mut (&mut concat).into(), &mut (&mut concat).into(),
("", Position::NONE), ("", Position::NONE),
item, item,
level,
) { ) {
result = Err(err.fill_position(expr.position())); result = Err(err.fill_position(expr.position()));
break; break;

View File

@ -120,6 +120,7 @@ impl Engine {
target: &mut Target, target: &mut Target,
root: (&str, Position), root: (&str, Position),
new_val: Dynamic, new_val: Dynamic,
level: usize,
) -> RhaiResultOf<()> { ) -> RhaiResultOf<()> {
if target.is_read_only() { if target.is_read_only() {
// Assignment to constant variable // Assignment to constant variable
@ -154,7 +155,7 @@ impl Engine {
let args = &mut [lhs_ptr_inner, &mut new_val]; let args = &mut [lhs_ptr_inner, &mut new_val];
match self.call_native_fn( match self.call_native_fn(
global, state, lib, op_assign, hash, args, true, true, op_pos, global, state, lib, op_assign, hash, args, true, true, op_pos, level,
) { ) {
Ok(_) => { Ok(_) => {
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
@ -164,7 +165,7 @@ impl Engine {
{ {
// Expand to `var = var op rhs` // Expand to `var = var op rhs`
let (value, _) = self.call_native_fn( let (value, _) = self.call_native_fn(
global, state, lib, op, hash_op, args, true, false, op_pos, global, state, lib, op, hash_op, args, true, false, op_pos, level,
)?; )?;
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
@ -266,6 +267,7 @@ impl Engine {
&mut lhs_ptr, &mut lhs_ptr,
(var_name, pos), (var_name, pos),
rhs_val, rhs_val,
level,
) )
.map_err(|err| err.fill_position(rhs.position())) .map_err(|err| err.fill_position(rhs.position()))
.map(|_| Dynamic::UNIT) .map(|_| Dynamic::UNIT)

View File

@ -328,6 +328,8 @@ impl Engine {
result.as_ref().map(Box::as_ref) result.as_ref().map(Box::as_ref)
} }
/// # Main Entry-Point
///
/// Call a native Rust function registered with the [`Engine`]. /// Call a native Rust function registered with the [`Engine`].
/// ///
/// # WARNING /// # WARNING
@ -347,6 +349,7 @@ impl Engine {
is_ref_mut: bool, is_ref_mut: bool,
is_op_assign: bool, is_op_assign: bool,
pos: Position, pos: Position,
level: usize,
) -> RhaiResultOf<(Dynamic, bool)> { ) -> RhaiResultOf<(Dynamic, bool)> {
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, pos)?; self.inc_operations(&mut global.num_operations, pos)?;
@ -365,39 +368,85 @@ impl Engine {
is_op_assign, is_op_assign,
); );
if let Some(FnResolutionCacheEntry { func, source }) = func { if func.is_some() {
assert!(func.is_native()); let is_method = func.map(|f| f.func.is_method()).unwrap_or(false);
// Calling pure function but the first argument is a reference? // Push a new call stack frame
let mut backup: Option<ArgBackup> = None; #[cfg(feature = "debugging")]
if is_ref_mut && func.is_pure() && !args.is_empty() { let orig_call_stack_len = global.debugger.call_stack().len();
// Clone the first argument
backup = Some(ArgBackup::new());
backup
.as_mut()
.expect("`Some`")
.change_first_arg_to_copy(args);
}
// Run external function let mut result = if let Some(FnResolutionCacheEntry { func, source }) = func {
let source = match (source.as_str(), parent_source.as_str()) { assert!(func.is_native());
("", "") => None,
("", s) | (s, _) => Some(s),
};
let context = (self, name, source, &*global, lib, pos).into(); // Calling pure function but the first argument is a reference?
let mut backup: Option<ArgBackup> = None;
if is_ref_mut && func.is_pure() && !args.is_empty() {
// Clone the first argument
backup = Some(ArgBackup::new());
backup
.as_mut()
.expect("`Some`")
.change_first_arg_to_copy(args);
}
let result = if func.is_plugin_fn() { let source = match (source.as_str(), parent_source.as_str()) {
func.get_plugin_fn() ("", "") => None,
.expect("plugin function") ("", s) | (s, _) => Some(s),
.call(context, args) };
#[cfg(feature = "debugging")]
if self.debugger.is_some() {
global.debugger.push_call_stack_frame(
name,
args.iter().map(|v| (*v).clone()).collect(),
source.unwrap_or(""),
pos,
);
}
// Run external function
let context = (self, name, source, &*global, lib, pos, level).into();
let result = if func.is_plugin_fn() {
func.get_plugin_fn()
.expect("plugin function")
.call(context, args)
} else {
func.get_native_fn().expect("native function")(context, args)
};
// Restore the original reference
if let Some(bk) = backup {
bk.restore_first_arg(args)
}
result
} else { } else {
func.get_native_fn().expect("native function")(context, args) unreachable!("`Some`");
}; };
// Restore the original reference #[cfg(feature = "debugging")]
if let Some(bk) = backup { if self.debugger.is_some() {
bk.restore_first_arg(args) match global.debugger.status() {
crate::eval::DebuggerStatus::FunctionExit(n) if n >= level => {
let scope = &mut &mut Scope::new();
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, &mut None, node, event, level,
) {
result = Err(err);
}
}
_ => (),
}
// Pop the call stack
global.debugger.rewind_call_stack(orig_call_stack_len);
} }
// Check the return value (including data sizes) // Check the return value (including data sizes)
@ -443,7 +492,7 @@ impl Engine {
(Dynamic::UNIT, false) (Dynamic::UNIT, false)
} }
} }
_ => (result, func.is_method()), _ => (result, is_method),
}); });
} }
@ -530,6 +579,8 @@ impl Engine {
} }
} }
/// # Main Entry-Point
///
/// Perform an actual function call, native Rust or scripted, taking care of special functions. /// Perform an actual function call, native Rust or scripted, taking care of special functions.
/// ///
/// # WARNING /// # WARNING
@ -562,7 +613,6 @@ impl Engine {
ensure_no_data_race(fn_name, args, is_ref_mut)?; ensure_no_data_race(fn_name, args, is_ref_mut)?;
let _scope = scope; let _scope = scope;
let _level = level;
let _is_method_call = is_method_call; let _is_method_call = is_method_call;
// These may be redirected from method style calls. // These may be redirected from method style calls.
@ -612,6 +662,8 @@ impl Engine {
_ => (), _ => (),
} }
let level = level + 1;
// Script-defined function call? // Script-defined function call?
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
if let Some(FnResolutionCacheEntry { func, mut source }) = self if let Some(FnResolutionCacheEntry { func, mut source }) = self
@ -646,7 +698,6 @@ impl Engine {
}; };
mem::swap(&mut global.source, &mut source); mem::swap(&mut global.source, &mut source);
let level = _level + 1;
let result = if _is_method_call { let result = if _is_method_call {
// Method call of script function - map first argument to `this` // Method call of script function - map first argument to `this`
@ -699,7 +750,7 @@ impl Engine {
// Native function call // Native function call
let hash = hashes.native; let hash = hashes.native;
self.call_native_fn( self.call_native_fn(
global, state, lib, fn_name, hash, args, is_ref_mut, false, pos, global, state, lib, fn_name, hash, args, is_ref_mut, false, pos, level,
) )
} }
@ -1318,6 +1369,8 @@ impl Engine {
} }
} }
let level = level + 1;
match func { match func {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
Some(f) if f.is_script() => { Some(f) if f.is_script() => {
@ -1331,8 +1384,6 @@ impl Engine {
let mut source = module.id_raw().clone(); let mut source = module.id_raw().clone();
mem::swap(&mut global.source, &mut source); mem::swap(&mut global.source, &mut source);
let level = level + 1;
let result = self.call_script_fn( let result = self.call_script_fn(
new_scope, global, state, lib, &mut None, fn_def, &mut args, pos, true, new_scope, global, state, lib, &mut None, fn_def, &mut args, pos, true,
level, level,
@ -1345,7 +1396,7 @@ impl Engine {
} }
Some(f) if f.is_plugin_fn() => { Some(f) if f.is_plugin_fn() => {
let context = (self, fn_name, module.id(), &*global, lib, pos).into(); let context = (self, fn_name, module.id(), &*global, lib, pos, level).into();
let result = f let result = f
.get_plugin_fn() .get_plugin_fn()
.expect("plugin function") .expect("plugin function")
@ -1356,7 +1407,7 @@ impl Engine {
Some(f) if f.is_native() => { Some(f) if f.is_native() => {
let func = f.get_native_fn().expect("native function"); let func = f.get_native_fn().expect("native function");
let context = (self, fn_name, module.id(), &*global, lib, pos).into(); let context = (self, fn_name, module.id(), &*global, lib, pos, level).into();
let result = func(context, &mut args); let result = func(context, &mut args);
self.check_return_value(result, pos) self.check_return_value(result, pos)
} }

View File

@ -70,6 +70,8 @@ pub struct NativeCallContext<'a> {
lib: &'a [&'a Module], lib: &'a [&'a Module],
/// [Position] of the function call. /// [Position] of the function call.
pos: Position, pos: Position,
/// The current nesting level of function calls.
level: usize,
} }
impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized> impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized>
@ -80,6 +82,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized>
&'a GlobalRuntimeState<'a>, &'a GlobalRuntimeState<'a>,
&'a M, &'a M,
Position, Position,
usize,
)> for NativeCallContext<'a> )> for NativeCallContext<'a>
{ {
#[inline(always)] #[inline(always)]
@ -91,6 +94,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized>
&'a GlobalRuntimeState, &'a GlobalRuntimeState,
&'a M, &'a M,
Position, Position,
usize,
), ),
) -> Self { ) -> Self {
Self { Self {
@ -100,6 +104,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized>
global: Some(value.3), global: Some(value.3),
lib: value.4.as_ref(), lib: value.4.as_ref(),
pos: value.5, pos: value.5,
level: value.6,
} }
} }
} }
@ -116,6 +121,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized>
global: None, global: None,
lib: value.2.as_ref(), lib: value.2.as_ref(),
pos: Position::NONE, pos: Position::NONE,
level: 0,
} }
} }
} }
@ -141,6 +147,7 @@ impl<'a> NativeCallContext<'a> {
global: None, global: None,
lib, lib,
pos: Position::NONE, pos: Position::NONE,
level: 0,
} }
} }
/// _(internals)_ Create a new [`NativeCallContext`]. /// _(internals)_ Create a new [`NativeCallContext`].
@ -158,6 +165,7 @@ impl<'a> NativeCallContext<'a> {
global: &'a GlobalRuntimeState, global: &'a GlobalRuntimeState,
lib: &'a [&Module], lib: &'a [&Module],
pos: Position, pos: Position,
level: usize,
) -> Self { ) -> Self {
Self { Self {
engine, engine,
@ -166,6 +174,7 @@ impl<'a> NativeCallContext<'a> {
global: Some(global), global: Some(global),
lib, lib,
pos, pos,
level,
} }
} }
/// The current [`Engine`]. /// The current [`Engine`].
@ -186,6 +195,12 @@ impl<'a> NativeCallContext<'a> {
pub const fn position(&self) -> Position { pub const fn position(&self) -> Position {
self.pos self.pos
} }
/// Current nesting level of function calls.
#[inline(always)]
#[must_use]
pub const fn call_level(&self) -> usize {
self.level
}
/// The current source. /// The current source.
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]
@ -316,7 +331,7 @@ impl<'a> NativeCallContext<'a> {
is_method_call, is_method_call,
Position::NONE, Position::NONE,
None, None,
0, self.level + 1,
) )
.map(|(r, _)| r) .map(|(r, _)| r)
} }

View File

@ -10,6 +10,8 @@ use std::mem;
use std::prelude::v1::*; use std::prelude::v1::*;
impl Engine { impl Engine {
/// # Main Entry-Point
///
/// Call a script-defined function. /// Call a script-defined function.
/// ///
/// If `rewind_scope` is `false`, arguments are removed from the scope but new variables are not. /// If `rewind_scope` is `false`, arguments are removed from the scope but new variables are not.
@ -90,16 +92,18 @@ impl Engine {
// Push a new call stack frame // Push a new call stack frame
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
global.debugger.push_call_stack_frame( if self.debugger.is_some() {
fn_def.name.clone(), global.debugger.push_call_stack_frame(
scope fn_def.name.clone(),
.iter() scope
.skip(orig_scope_len) .iter()
.map(|(_, _, v)| v.clone()) .skip(orig_scope_len)
.collect(), .map(|(_, _, v)| v.clone())
global.source.clone(), .collect(),
pos, global.source.clone(),
); pos,
);
}
// Merge in encapsulated environment, if any // Merge in encapsulated environment, if any
let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len();
@ -167,26 +171,24 @@ impl Engine {
}); });
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
{ if self.debugger.is_some() {
if self.debugger.is_some() { match global.debugger.status() {
match global.debugger.status() { crate::eval::DebuggerStatus::FunctionExit(n) if n >= level => {
crate::eval::DebuggerStatus::FunctionExit(n) if n >= level => { let node = crate::ast::Stmt::Noop(pos);
let node = crate::ast::Stmt::Noop(pos); let node = (&node).into();
let node = (&node).into(); let event = match result {
let event = match result { Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r),
Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r), Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err),
Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err), };
}; if let Err(err) = self
if let Err(err) = self.run_debugger_raw( .run_debugger_raw(scope, global, state, lib, this_ptr, node, event, level)
scope, global, state, lib, this_ptr, node, event, level, {
) { result = Err(err);
result = Err(err);
}
} }
_ => (),
} }
_ => (),
} }
// Pop the call stack // Pop the call stack
global.debugger.rewind_call_stack(orig_call_stack_len); global.debugger.rewind_call_stack(orig_call_stack_len);
} }

View File

@ -147,6 +147,7 @@ impl<'a> OptimizerState<'a> {
false, false,
false, false,
Position::NONE, Position::NONE,
0,
) )
.ok() .ok()
.map(|(v, _)| v) .map(|(v, _)| v)

View File

@ -27,15 +27,23 @@ def_package! {
#[export_module] #[export_module]
mod debugging_functions { mod debugging_functions {
/// Get an array of object maps containing the function calls stack.
///
/// If there is no debugging interface registered, an empty array is returned.
///
/// An array of strings is returned under `no_object`.
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
pub fn stack_trace(ctx: NativeCallContext) -> Array { pub fn back_trace(ctx: NativeCallContext) -> Array {
if let Some(global) = ctx.global_runtime_state() { if let Some(global) = ctx.global_runtime_state() {
global global
.debugger .debugger
.call_stack() .call_stack()
.iter() .iter()
.rev() .rev()
.filter(|crate::debugger::CallStackFrame { fn_name, args, .. }| {
fn_name != "back_trace" || !args.is_empty()
})
.map( .map(
|frame @ crate::debugger::CallStackFrame { |frame @ crate::debugger::CallStackFrame {
fn_name: _fn_name, fn_name: _fn_name,

View File

@ -37,9 +37,9 @@ fn check_struct_sizes() {
assert_eq!( assert_eq!(
size_of::<NativeCallContext>(), size_of::<NativeCallContext>(),
if cfg!(feature = "no_position") { if cfg!(feature = "no_position") {
64
} else {
72 72
} else {
80
} }
); );
} }

View File

@ -18,7 +18,7 @@ fn test_debugging() -> Result<(), Box<EvalAltResult>> {
" "
fn foo(x) { fn foo(x) {
if x >= 5 { if x >= 5 {
stack_trace() back_trace()
} else { } else {
foo(x+1) foo(x+1)
} }
@ -30,7 +30,7 @@ fn test_debugging() -> Result<(), Box<EvalAltResult>> {
assert_eq!(r.len(), 6); assert_eq!(r.len(), 6);
assert_eq!(engine.eval::<INT>("len(stack_trace())")?, 0); assert_eq!(engine.eval::<INT>("len(back_trace())")?, 0);
} }
Ok(()) Ok(())