From 4a80483749149b8b873298d82c2f4c4d802f10b9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 2 Feb 2022 14:47:35 +0800 Subject: [PATCH] Support call stack and FunctionExit for native functions. --- CHANGELOG.md | 3 +- src/bin/rhai-dbg.rs | 24 +++++++- src/eval/chaining.rs | 3 + src/eval/debugger.rs | 19 ++++-- src/eval/expr.rs | 1 + src/eval/stmt.rs | 6 +- src/func/call.rs | 119 +++++++++++++++++++++++++++----------- src/func/native.rs | 17 +++++- src/func/script.rs | 56 +++++++++--------- src/optimizer.rs | 1 + src/packages/debugging.rs | 10 +++- src/tests.rs | 4 +- tests/debugging.rs | 4 +- 13 files changed, 190 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0afcd1a1..0de82a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ----------------- diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs index c90881cf..5158eebb 100644 --- a/src/bin/rhai-dbg.rs +++ b/src/bin/rhai-dbg.rs @@ -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 + ) } } diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index 43a4e4a8..16daa9e5 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -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))?; diff --git a/src/eval/debugger.rs b/src/eval/debugger.rs index 28c86e4d..d2d2234d 100644 --- a/src/eval/debugger.rs +++ b/src/eval/debugger.rs @@ -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) } } diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 0b848b9c..3b6015b7 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -353,6 +353,7 @@ impl Engine { &mut (&mut concat).into(), ("", Position::NONE), item, + level, ) { result = Err(err.fill_position(expr.position())); break; diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index b853d3dc..2adb7829 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -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) diff --git a/src/func/call.rs b/src/func/call.rs index 612982e4..a73bd7bb 100644 --- a/src/func/call.rs +++ b/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 = 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 = 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) } diff --git a/src/func/native.rs b/src/func/native.rs index 7938f4cd..c5787386 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -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 + 'a + ?Sized> @@ -80,6 +82,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + '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 + 'a + ?Sized> &'a GlobalRuntimeState, &'a M, Position, + usize, ), ) -> Self { Self { @@ -100,6 +104,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + '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 + '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) } diff --git a/src/func/script.rs b/src/func/script.rs index 650f9db2..5178ed2b 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -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); } diff --git a/src/optimizer.rs b/src/optimizer.rs index d430ac76..acae357e 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -147,6 +147,7 @@ impl<'a> OptimizerState<'a> { false, false, Position::NONE, + 0, ) .ok() .map(|(v, _)| v) diff --git a/src/packages/debugging.rs b/src/packages/debugging.rs index 397f8b8e..ab24bc3a 100644 --- a/src/packages/debugging.rs +++ b/src/packages/debugging.rs @@ -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, diff --git a/src/tests.rs b/src/tests.rs index f4d32675..e96d8dd5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -37,9 +37,9 @@ fn check_struct_sizes() { assert_eq!( size_of::(), if cfg!(feature = "no_position") { - 64 - } else { 72 + } else { + 80 } ); } diff --git a/tests/debugging.rs b/tests/debugging.rs index 036bd4fb..b40c1016 100644 --- a/tests/debugging.rs +++ b/tests/debugging.rs @@ -18,7 +18,7 @@ fn test_debugging() -> Result<(), Box> { " fn foo(x) { if x >= 5 { - stack_trace() + back_trace() } else { foo(x+1) } @@ -30,7 +30,7 @@ fn test_debugging() -> Result<(), Box> { assert_eq!(r.len(), 6); - assert_eq!(engine.eval::("len(stack_trace())")?, 0); + assert_eq!(engine.eval::("len(back_trace())")?, 0); } Ok(())