Support call stack and FunctionExit for native functions.
This commit is contained in:
parent
7163a7331a
commit
4a80483749
@ -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
|
||||
-----------------
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))?;
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -353,6 +353,7 @@ impl Engine {
|
||||
&mut (&mut concat).into(),
|
||||
("", Position::NONE),
|
||||
item,
|
||||
level,
|
||||
) {
|
||||
result = Err(err.fill_position(expr.position()));
|
||||
break;
|
||||
|
@ -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)
|
||||
|
119
src/func/call.rs
119
src/func/call.rs
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -147,6 +147,7 @@ impl<'a> OptimizerState<'a> {
|
||||
false,
|
||||
false,
|
||||
Position::NONE,
|
||||
0,
|
||||
)
|
||||
.ok()
|
||||
.map(|(v, _)| v)
|
||||
|
@ -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,
|
||||
|
@ -37,9 +37,9 @@ fn check_struct_sizes() {
|
||||
assert_eq!(
|
||||
size_of::<NativeCallContext>(),
|
||||
if cfg!(feature = "no_position") {
|
||||
64
|
||||
} else {
|
||||
72
|
||||
} else {
|
||||
80
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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(())
|
||||
|
Loading…
Reference in New Issue
Block a user