rhai 1.26.0

Embedded scripting for Rust
Documentation
//! Implement script function-calling mechanism for [`Engine`].
#![cfg(not(feature = "no_function"))]

use super::call::FnCallArgs;
use crate::ast::{EncapsulatedEnviron, ScriptFuncDef, ScriptFuncPayload};
use crate::eval::{Caches, GlobalRuntimeState};
use crate::{Dynamic, Engine, FnArgsVec, Position, RhaiResult, Scope, ERR};
#[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>,
        _env: Option<&EncapsulatedEnviron>,
        fn_def: &ScriptFuncDef,
        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
        #[cfg(not(feature = "unchecked"))]
        if global.level > self.max_call_levels() {
            return Err(ERR::ErrorStackOverflow(pos).into());
        }

        // Guard against too many variables
        #[cfg(not(feature = "unchecked"))]
        if scope.len() + fn_def.params.len() > self.max_variables() {
            return Err(ERR::ErrorTooManyVariables(pos).into());
        }

        // Short-circuit empty function body
        match fn_def.body {
            ScriptFuncPayload::Statements(ref block) => {
                let is_empty = block.is_empty();
                #[cfg(feature = "debugging")]
                let is_empty = is_empty && self.debugger_interface.is_none();

                if is_empty {
                    return Ok(Dynamic::UNIT);
                }
            }
            // We don't know about Grain functions, so call it anyway
            #[cfg(feature = "grain")]
            ScriptFuncPayload::GrainVM { .. } => (),
        }

        // Save the original state
        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 collect function call arguments.
        // Actually consume the arguments instead of cloning them.
        let arg_values = args.iter_mut().map(|v| v.take()).collect::<FnArgsVec<_>>();

        // Push a new call stack frame
        #[cfg(feature = "debugging")]
        if self.is_debugger_registered() {
            let fn_name = fn_def.name.clone();
            let args = arg_values
                .iter()
                .map(Dynamic::flatten_clone)
                .collect::<FnArgsVec<_>>();
            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 = _env.map(
            |EncapsulatedEnviron {

                 lib,
                 imports,
                 constants,
             }| {
                imports
                    .iter()
                    .cloned()
                    .for_each(|(n, m)| global.push_import(n, m));

                global.lib.extend(lib.clone());

                std::mem::replace(&mut global.constants, constants.clone())
            },
        );

        let mut _arg_slots = 0;

        // Evaluate the function
        let mut _result: RhaiResult = match fn_def.body {
            // Normal statements block
            ScriptFuncPayload::Statements(ref body) => {
                // Put arguments into scope as variables
                scope.extend(fn_def.params.iter().cloned().zip(arg_values));
                _arg_slots = fn_def.params.len();

                #[cfg(feature = "debugging")]
                if self.is_debugger_registered() {
                    let node = crate::ast::Stmt::Noop(fn_def.body.start_position());
                    self.dbg(global, caches, scope, this_ptr.as_deref_mut(), &node)?;
                }

                self.eval_stmt_block(
                    global,
                    caches,
                    scope,
                    this_ptr.as_deref_mut(),
                    body.statements(),
                    rewind_scope,
                )
                .or_else(|err| match *err {
                    // Convert return statement to return value
                    ERR::Return(x, ..) => Ok(x),
                    // Exit value is passed straight-through
                    mut err @ ERR::Exit(..) => {
                        err.set_position(pos);
                        Err(err.into())
                    }
                    // 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"))]
                        _env.and_then(|env| env.lib.last())
                            .and_then(|m| m.id())
                            .unwrap_or_else(|| global.source().unwrap_or(""))
                            .to_string(),
                        #[cfg(feature = "no_module")]
                        global.source().unwrap_or("").to_string(),
                        err,
                        pos,
                    )
                    .into()),
                })
            }
            // Rhai Grain VM
            #[cfg(feature = "grain")]
            ScriptFuncPayload::GrainVM {
                ref program,
                ref params,
                chunk,
                ..
            } => {
                let context = (self, fn_def.name.as_str(), global.source(), &*global, pos).into();
                let mut vm = crate::grain::Vm::reentrant(&context);

                // The value in the `this` pointer is cloned
                let this_ptr_value = this_ptr.as_deref_mut().cloned();

                let (result, new_this_ptr) = vm.call_function_with_this(
                    program,
                    fn_def.name.as_str(),
                    params,
                    chunk,
                    arg_values,
                    global.level,
                    scope,
                    rewind_scope,
                    pos,
                    this_ptr_value,
                );

                // Write back new value for the `this` pointer
                if let Some(this_ptr) = this_ptr.as_deref_mut() {
                    if let Some(new_this_ptr) = new_this_ptr {
                        *this_ptr = new_this_ptr;
                    }
                }

                result
            }
        };

        #[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.dbg_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 _arg_slots > 0 {
            // Remove arguments only, leaving new variables in the scope
            scope.remove_range(orig_scope_len, _arg_slots);
        }
        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?
    ///
    /// # Note
    ///
    /// If the scripted function is not found, this information is cached for future look-ups.
    #[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.dict.get(&hash_script).map(Option::is_some) {
            return result;
        }

        // First check script-defined functions
        let result = 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 result = result ||
            // Then check imported modules
            global.contains_qualified_fn(hash_script)
            // Then check sub-modules
            || self.global_sub_modules.values().any(|m| m.contains_qualified_fn(hash_script));

        if !result && !cache.bloom_filter.is_absent_and_set(hash_script) {
            // Do not cache "one-hit wonders"
            cache.dict.insert(hash_script, None);
        }

        result
    }
}