234 lines
8.0 KiB
Rust
234 lines
8.0 KiB
Rust
//! Implement script function-calling mechanism for [`Engine`].
|
|
#![cfg(not(feature = "no_function"))]
|
|
|
|
use super::call::FnCallArgs;
|
|
use crate::ast::ScriptFnDef;
|
|
use crate::eval::{Caches, GlobalRuntimeState};
|
|
use crate::func::EncapsulatedEnviron;
|
|
use crate::{Dynamic, Engine, Position, RhaiResult, Scope, ERR};
|
|
use std::mem;
|
|
#[cfg(feature = "no_std")]
|
|
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.
|
|
///
|
|
/// # WARNING
|
|
///
|
|
/// Function call arguments may be _consumed_ when the function requires them to be passed by value.
|
|
/// All function arguments not in the first position are always passed by value and thus consumed.
|
|
///
|
|
/// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
|
|
pub(crate) fn call_script_fn(
|
|
&self,
|
|
global: &mut GlobalRuntimeState,
|
|
caches: &mut Caches,
|
|
scope: &mut Scope,
|
|
mut this_ptr: Option<&mut Dynamic>,
|
|
_environ: Option<&EncapsulatedEnviron>,
|
|
fn_def: &ScriptFnDef,
|
|
args: &mut FnCallArgs,
|
|
rewind_scope: bool,
|
|
pos: Position,
|
|
) -> RhaiResult {
|
|
debug_assert_eq!(fn_def.params.len(), args.len());
|
|
|
|
self.track_operation(global, pos)?;
|
|
|
|
// Check for stack overflow
|
|
if global.level > self.max_call_levels() {
|
|
return Err(ERR::ErrorStackOverflow(pos).into());
|
|
}
|
|
|
|
#[cfg(feature = "debugging")]
|
|
if self.debugger_interface.is_none() && fn_def.body.is_empty() {
|
|
return Ok(Dynamic::UNIT);
|
|
}
|
|
#[cfg(not(feature = "debugging"))]
|
|
if fn_def.body.is_empty() {
|
|
return Ok(Dynamic::UNIT);
|
|
}
|
|
|
|
let orig_scope_len = scope.len();
|
|
let orig_lib_len = global.lib.len();
|
|
#[cfg(not(feature = "no_module"))]
|
|
let orig_imports_len = global.num_imports();
|
|
|
|
#[cfg(feature = "debugging")]
|
|
let orig_call_stack_len = global
|
|
.debugger
|
|
.as_ref()
|
|
.map_or(0, |dbg| dbg.call_stack().len());
|
|
|
|
// Put arguments into scope as variables
|
|
scope.extend(fn_def.params.iter().cloned().zip(args.iter_mut().map(|v| {
|
|
// Actually consume the arguments instead of cloning them
|
|
v.take()
|
|
})));
|
|
|
|
// Push a new call stack frame
|
|
#[cfg(feature = "debugging")]
|
|
if self.is_debugger_registered() {
|
|
let fn_name = fn_def.name.clone();
|
|
let args = scope.iter().skip(orig_scope_len).map(|(.., v)| v).collect();
|
|
let source = global.source.clone();
|
|
|
|
global
|
|
.debugger_mut()
|
|
.push_call_stack_frame(fn_name, args, source, pos);
|
|
}
|
|
|
|
// Merge in encapsulated environment, if any
|
|
let orig_fn_resolution_caches_len = caches.fn_resolution_caches_len();
|
|
|
|
#[cfg(not(feature = "no_module"))]
|
|
let orig_constants = _environ.map(|environ| {
|
|
let EncapsulatedEnviron {
|
|
lib,
|
|
imports,
|
|
constants,
|
|
} = environ;
|
|
|
|
imports
|
|
.iter()
|
|
.cloned()
|
|
.for_each(|(n, m)| global.push_import(n, m));
|
|
|
|
global.lib.push(lib.clone());
|
|
|
|
mem::replace(&mut global.constants, constants.clone())
|
|
});
|
|
|
|
#[cfg(feature = "debugging")]
|
|
if self.is_debugger_registered() {
|
|
let node = crate::ast::Stmt::Noop(fn_def.body.position());
|
|
self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), &node)?;
|
|
}
|
|
|
|
// Evaluate the function
|
|
let mut _result: RhaiResult = self
|
|
.eval_stmt_block(
|
|
global,
|
|
caches,
|
|
scope,
|
|
this_ptr.as_deref_mut(),
|
|
&fn_def.body,
|
|
rewind_scope,
|
|
)
|
|
.or_else(|err| match *err {
|
|
// Convert return statement to return value
|
|
ERR::Return(x, ..) => Ok(x),
|
|
// System errors are passed straight-through
|
|
mut err if err.is_system_exception() => {
|
|
err.set_position(pos);
|
|
Err(err.into())
|
|
}
|
|
// Other errors are wrapped in `ErrorInFunctionCall`
|
|
_ => Err(ERR::ErrorInFunctionCall(
|
|
fn_def.name.to_string(),
|
|
#[cfg(not(feature = "no_module"))]
|
|
_environ
|
|
.and_then(|environ| environ.lib.id())
|
|
.unwrap_or_else(|| global.source().unwrap_or(""))
|
|
.to_string(),
|
|
#[cfg(feature = "no_module")]
|
|
global.source().unwrap_or("").to_string(),
|
|
err,
|
|
pos,
|
|
)
|
|
.into()),
|
|
});
|
|
|
|
#[cfg(feature = "debugging")]
|
|
if self.is_debugger_registered() {
|
|
let trigger = match global.debugger_mut().status {
|
|
crate::eval::DebuggerStatus::FunctionExit(n) => n >= global.level,
|
|
crate::eval::DebuggerStatus::Next(.., true) => true,
|
|
_ => false,
|
|
};
|
|
|
|
if trigger {
|
|
let node = crate::ast::Stmt::Noop(fn_def.body.end_position().or_else(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),
|
|
};
|
|
match self.run_debugger_raw(global, caches, scope, this_ptr, node, event) {
|
|
Ok(_) => (),
|
|
Err(err) => _result = Err(err),
|
|
}
|
|
}
|
|
|
|
// Pop the call stack
|
|
global
|
|
.debugger
|
|
.as_mut()
|
|
.unwrap()
|
|
.rewind_call_stack(orig_call_stack_len);
|
|
}
|
|
|
|
// Remove all local variables and imported modules
|
|
if rewind_scope {
|
|
scope.rewind(orig_scope_len);
|
|
} else if !args.is_empty() {
|
|
// Remove arguments only, leaving new variables in the scope
|
|
scope.remove_range(orig_scope_len, args.len());
|
|
}
|
|
global.lib.truncate(orig_lib_len);
|
|
#[cfg(not(feature = "no_module"))]
|
|
global.truncate_imports(orig_imports_len);
|
|
|
|
// Restore constants
|
|
#[cfg(not(feature = "no_module"))]
|
|
if let Some(constants) = orig_constants {
|
|
global.constants = constants;
|
|
}
|
|
|
|
// Restore state
|
|
caches.rewind_fn_resolution_caches(orig_fn_resolution_caches_len);
|
|
|
|
_result
|
|
}
|
|
|
|
// Does a script-defined function exist?
|
|
#[must_use]
|
|
pub(crate) fn has_script_fn(
|
|
&self,
|
|
global: &GlobalRuntimeState,
|
|
caches: &mut Caches,
|
|
hash_script: u64,
|
|
) -> bool {
|
|
let cache = caches.fn_resolution_cache_mut();
|
|
|
|
if let Some(result) = cache.map.get(&hash_script).map(Option::is_some) {
|
|
return result;
|
|
}
|
|
|
|
// First check script-defined functions
|
|
let r = global.lib.iter().any(|m| m.contains_fn(hash_script))
|
|
// Then check the global namespace and packages
|
|
|| self.global_modules.iter().any(|m| m.contains_fn(hash_script));
|
|
|
|
#[cfg(not(feature = "no_module"))]
|
|
let r = r ||
|
|
// Then check imported modules
|
|
global.contains_qualified_fn(hash_script)
|
|
// Then check sub-modules
|
|
|| self.global_sub_modules.as_ref().map_or(false, |m| {
|
|
m.values().any(|m| m.contains_qualified_fn(hash_script))
|
|
});
|
|
|
|
if !r && !cache.filter.is_absent_and_set(hash_script) {
|
|
// Do not cache "one-hit wonders"
|
|
cache.map.insert(hash_script, None);
|
|
}
|
|
|
|
r
|
|
}
|
|
}
|