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

@@ -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);
}