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 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
------------
@ -32,6 +32,7 @@ Enhancements
* 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.
* Debug display of `AST` is improved.
* `NativeCallContext::call_level()` is added to give the current nesting level of function calls.
REPL tool changes
-----------------

View File

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

View File

@ -346,13 +346,15 @@ impl Debugger {
node.position() == *pos && _src == source
}
BreakPoint::AtFunctionName { name, .. } => match node {
ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => {
x.name == *name
}
ASTNode::Expr(Expr::FnCall(x, _))
| ASTNode::Stmt(Stmt::FnCall(x, _))
| ASTNode::Stmt(Stmt::Expr(Expr::FnCall(x, _))) => x.name == *name,
_ => false,
},
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
}
_ => false,
@ -503,7 +505,14 @@ impl Engine {
Ok(None)
}
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)
}
}

View File

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

View File

@ -120,6 +120,7 @@ impl Engine {
target: &mut Target,
root: (&str, Position),
new_val: Dynamic,
level: usize,
) -> RhaiResultOf<()> {
if target.is_read_only() {
// Assignment to constant variable
@ -154,7 +155,7 @@ impl Engine {
let args = &mut [lhs_ptr_inner, &mut new_val];
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(_) => {
#[cfg(not(feature = "unchecked"))]
@ -164,7 +165,7 @@ impl Engine {
{
// Expand to `var = var op rhs`
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"))]
@ -266,6 +267,7 @@ impl Engine {
&mut lhs_ptr,
(var_name, pos),
rhs_val,
level,
)
.map_err(|err| err.fill_position(rhs.position()))
.map(|_| Dynamic::UNIT)

View File

@ -328,6 +328,8 @@ impl Engine {
result.as_ref().map(Box::as_ref)
}
/// # Main Entry-Point
///
/// Call a native Rust function registered with the [`Engine`].
///
/// # WARNING
@ -347,6 +349,7 @@ impl Engine {
is_ref_mut: bool,
is_op_assign: bool,
pos: Position,
level: usize,
) -> RhaiResultOf<(Dynamic, bool)> {
#[cfg(not(feature = "unchecked"))]
self.inc_operations(&mut global.num_operations, pos)?;
@ -365,39 +368,85 @@ impl Engine {
is_op_assign,
);
if let Some(FnResolutionCacheEntry { func, source }) = func {
assert!(func.is_native());
if func.is_some() {
let is_method = func.map(|f| f.func.is_method()).unwrap_or(false);
// 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);
}
// Push a new call stack frame
#[cfg(feature = "debugging")]
let orig_call_stack_len = global.debugger.call_stack().len();
// Run external function
let source = match (source.as_str(), parent_source.as_str()) {
("", "") => None,
("", s) | (s, _) => Some(s),
};
let mut result = if let Some(FnResolutionCacheEntry { func, source }) = func {
assert!(func.is_native());
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() {
func.get_plugin_fn()
.expect("plugin function")
.call(context, args)
let source = match (source.as_str(), parent_source.as_str()) {
("", "") => None,
("", s) | (s, _) => Some(s),
};
#[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 {
func.get_native_fn().expect("native function")(context, args)
unreachable!("`Some`");
};
// Restore the original reference
if let Some(bk) = backup {
bk.restore_first_arg(args)
#[cfg(feature = "debugging")]
if self.debugger.is_some() {
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)
@ -443,7 +492,7 @@ impl Engine {
(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.
///
/// # WARNING
@ -562,7 +613,6 @@ impl Engine {
ensure_no_data_race(fn_name, args, is_ref_mut)?;
let _scope = scope;
let _level = level;
let _is_method_call = is_method_call;
// These may be redirected from method style calls.
@ -612,6 +662,8 @@ impl Engine {
_ => (),
}
let level = level + 1;
// Script-defined function call?
#[cfg(not(feature = "no_function"))]
if let Some(FnResolutionCacheEntry { func, mut source }) = self
@ -646,7 +698,6 @@ impl Engine {
};
mem::swap(&mut global.source, &mut source);
let level = _level + 1;
let result = if _is_method_call {
// Method call of script function - map first argument to `this`
@ -699,7 +750,7 @@ impl Engine {
// Native function call
let hash = hashes.native;
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 {
#[cfg(not(feature = "no_function"))]
Some(f) if f.is_script() => {
@ -1331,8 +1384,6 @@ impl Engine {
let mut source = module.id_raw().clone();
mem::swap(&mut global.source, &mut source);
let level = level + 1;
let result = self.call_script_fn(
new_scope, global, state, lib, &mut None, fn_def, &mut args, pos, true,
level,
@ -1345,7 +1396,7 @@ impl Engine {
}
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
.get_plugin_fn()
.expect("plugin function")
@ -1356,7 +1407,7 @@ impl Engine {
Some(f) if f.is_native() => {
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);
self.check_return_value(result, pos)
}

View File

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

View File

@ -10,6 +10,8 @@ use std::mem;
use std::prelude::v1::*;
impl Engine {
/// # Main Entry-Point
///
/// Call a script-defined function.
///
/// 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
#[cfg(feature = "debugging")]
global.debugger.push_call_stack_frame(
fn_def.name.clone(),
scope
.iter()
.skip(orig_scope_len)
.map(|(_, _, v)| v.clone())
.collect(),
global.source.clone(),
pos,
);
if self.debugger.is_some() {
global.debugger.push_call_stack_frame(
fn_def.name.clone(),
scope
.iter()
.skip(orig_scope_len)
.map(|(_, _, v)| v.clone())
.collect(),
global.source.clone(),
pos,
);
}
// Merge in encapsulated environment, if any
let orig_fn_resolution_caches_len = state.fn_resolution_caches_len();
@ -167,26 +171,24 @@ impl Engine {
});
#[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);
}
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);
}

View File

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

View File

@ -27,15 +27,23 @@ def_package! {
#[export_module]
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_index"))]
pub fn stack_trace(ctx: NativeCallContext) -> Array {
pub fn back_trace(ctx: NativeCallContext) -> Array {
if let Some(global) = ctx.global_runtime_state() {
global
.debugger
.call_stack()
.iter()
.rev()
.filter(|crate::debugger::CallStackFrame { fn_name, args, .. }| {
fn_name != "back_trace" || !args.is_empty()
})
.map(
|frame @ crate::debugger::CallStackFrame {
fn_name: _fn_name,

View File

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

View File

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