//! Implement function-calling mechanism for [`Engine`]. use super::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn, CallableFunction}; use crate::api::default_limits::MAX_DYNAMIC_PARAMETERS; use crate::ast::{Expr, FnCallExpr, FnCallHashes}; use crate::engine::{ KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF, }; use crate::eval::{Caches, FnResolutionCacheEntry, GlobalRuntimeState}; use crate::tokenizer::{is_valid_function_name, Token}; use crate::{ calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnArgsVec, FnPtr, ImmutableString, OptimizationLevel, Position, RhaiError, RhaiResult, RhaiResultOf, Scope, Shared, ERR, }; #[cfg(feature = "no_std")] use hashbrown::hash_map::Entry; #[cfg(not(feature = "no_std"))] use std::collections::hash_map::Entry; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ any::{type_name, TypeId}, convert::TryFrom, mem, }; /// Arguments to a function call, which is a list of [`&mut Dynamic`][Dynamic]. pub type FnCallArgs<'a> = [&'a mut Dynamic]; /// A type that temporarily stores a mutable reference to a `Dynamic`, /// replacing it with a cloned copy. #[derive(Debug)] struct ArgBackup<'a> { orig_mut: Option<&'a mut Dynamic>, value_copy: Dynamic, } impl<'a> ArgBackup<'a> { /// Create a new `ArgBackup`. #[inline(always)] pub fn new() -> Self { Self { orig_mut: None, value_copy: Dynamic::UNIT, } } /// This function replaces the first argument of a method call with a clone copy. /// This is to prevent a pure function unintentionally consuming the first argument. /// /// `restore_first_arg` must be called before the end of the scope to prevent the shorter /// lifetime from leaking. /// /// # Safety /// /// This method blindly casts a reference to another lifetime, which saves allocation and /// string cloning. /// /// As long as `restore_first_arg` is called before the end of the scope, the shorter lifetime /// will not leak. /// /// # Panics /// /// Panics when `args` is empty. #[inline(always)] pub fn change_first_arg_to_copy(&mut self, args: &mut FnCallArgs<'a>) { // Clone the original value. self.value_copy = args[0].clone(); // Replace the first reference with a reference to the clone, force-casting the lifetime. // Must remember to restore it later with `restore_first_arg`. // // SAFETY: // // Blindly casting a reference to another lifetime saves allocation and string cloning, // but must be used with the utmost care. // // We can do this here because, before the end of this scope, we'd restore the original // reference via `restore_first_arg`. Therefore this shorter lifetime does not leak. self.orig_mut = Some(mem::replace(&mut args[0], unsafe { mem::transmute(&mut self.value_copy) })); } /// This function restores the first argument that was replaced by `change_first_arg_to_copy`. /// /// # Safety /// /// If `change_first_arg_to_copy` has been called, this function **MUST** be called _BEFORE_ /// exiting the current scope. Otherwise it is undefined behavior as the shorter lifetime will leak. #[inline(always)] pub fn restore_first_arg(&mut self, args: &mut FnCallArgs<'a>) { args[0] = self.orig_mut.take().expect("`Some`"); } } impl Drop for ArgBackup<'_> { #[inline(always)] fn drop(&mut self) { // Panic if the shorter lifetime leaks. assert!( self.orig_mut.is_none(), "ArgBackup::restore_first_arg has not been called prior to existing this scope" ); } } // Ensure no data races in function call arguments. #[cfg(not(feature = "no_closure"))] #[inline] pub fn ensure_no_data_race(fn_name: &str, args: &FnCallArgs, is_ref_mut: bool) -> RhaiResultOf<()> { if let Some((n, ..)) = args .iter() .enumerate() .skip(usize::from(is_ref_mut)) .find(|(.., a)| a.is_locked()) { return Err(ERR::ErrorDataRace( format!("argument #{} of function '{fn_name}'", n + 1), Position::NONE, ) .into()); } Ok(()) } /// Is a function name an anonymous function? #[cfg(not(feature = "no_function"))] #[inline] #[must_use] pub fn is_anonymous_fn(name: &str) -> bool { name.starts_with(crate::engine::FN_ANONYMOUS) } impl Engine { /// Generate the signature for a function call. #[inline] #[must_use] fn gen_fn_call_signature(&self, fn_name: &str, args: &[&mut Dynamic]) -> String { format!( "{fn_name} ({})", args.iter() .map(|a| if a.is_string() { "&str | ImmutableString | String" } else { self.map_type_name(a.type_name()) }) .collect::>() .join(", ") ) } /// Resolve a normal (non-qualified) function call. /// /// Search order: /// 1) AST - script functions in the AST /// 2) Global namespace - functions registered via `Engine::register_XXX` /// 3) Global registered modules - packages /// 4) Imported modules - functions marked with global namespace /// 5) Static registered modules #[must_use] fn resolve_fn<'s>( &self, _global: &GlobalRuntimeState, caches: &'s mut Caches, local_entry: &'s mut Option, op_token: Option, hash_base: u64, args: Option<&mut FnCallArgs>, allow_dynamic: bool, ) -> Option<&'s FnResolutionCacheEntry> { if hash_base == 0 { return None; } let mut hash = args.as_deref().map_or(hash_base, |args| { calc_fn_hash_full(hash_base, args.iter().map(|a| a.type_id())) }); let cache = caches.fn_resolution_cache_mut(); match cache.map.entry(hash) { Entry::Occupied(entry) => entry.into_mut().as_ref(), Entry::Vacant(entry) => { let num_args = args.as_deref().map_or(0, FnCallArgs::len); let mut max_bitmask = 0; // One above maximum bitmask based on number of parameters. // Set later when a specific matching function is not found. let mut bitmask = 1usize; // Bitmask of which parameter to replace with `Dynamic` loop { #[cfg(not(feature = "no_function"))] let func = _global .lib .iter() .rev() .chain(self.global_modules.iter()) .find_map(|m| m.get_fn(hash).map(|f| (f, m.id_raw()))); #[cfg(feature = "no_function")] let func = None; let func = func.or_else(|| { self.global_modules .iter() .find_map(|m| m.get_fn(hash).map(|f| (f, m.id_raw()))) }); #[cfg(not(feature = "no_module"))] let func = if args.is_none() { // Scripted functions are not exposed globally func } else { func.or_else(|| _global.get_qualified_fn(hash)).or_else(|| { self.global_sub_modules .as_deref() .into_iter() .flatten() .find_map(|(_, m)| { m.get_qualified_fn(hash).map(|f| (f, m.id_raw())) }) }) }; if let Some((f, s)) = func { // Specific version found let new_entry = FnResolutionCacheEntry { func: f.clone(), source: s.cloned(), }; return if cache.filter.is_absent_and_set(hash) { // Do not cache "one-hit wonders" *local_entry = Some(new_entry); local_entry.as_ref() } else { // Cache entry entry.insert(Some(new_entry)).as_ref() }; } // Check `Dynamic` parameters for functions with parameters if allow_dynamic && max_bitmask == 0 && num_args > 0 { let is_dynamic = self .global_modules .iter() .any(|m| m.may_contain_dynamic_fn(hash_base)); #[cfg(not(feature = "no_function"))] let is_dynamic = is_dynamic || _global .lib .iter() .any(|m| m.may_contain_dynamic_fn(hash_base)); #[cfg(not(feature = "no_module"))] let is_dynamic = is_dynamic || _global.may_contain_dynamic_fn(hash_base) || self.global_sub_modules.as_deref().map_or(false, |m| { m.values().any(|m| m.may_contain_dynamic_fn(hash_base)) }); // Set maximum bitmask when there are dynamic versions of the function if is_dynamic { max_bitmask = 1usize << usize::min(num_args, MAX_DYNAMIC_PARAMETERS); } } // Stop when all permutations are exhausted if bitmask >= max_bitmask { if num_args != 2 { return None; } // Try to find a built-in version let builtin = args.and_then(|args| match op_token { None => None, Some(token) if token.is_op_assignment() => { let (first_arg, rest_args) = args.split_first().unwrap(); get_builtin_op_assignment_fn(token, first_arg, rest_args[0]) .map(|(f, has_context)| FnResolutionCacheEntry { func: CallableFunction::Method { func: Shared::new(f), has_context, }, source: None, }) } Some(token) => get_builtin_binary_op_fn(token, args[0], args[1]) .map(|(f, has_context)| FnResolutionCacheEntry { func: CallableFunction::Method { func: Shared::new(f), has_context, }, source: None, }), }); return if cache.filter.is_absent_and_set(hash) { // Do not cache "one-hit wonders" *local_entry = builtin; local_entry.as_ref() } else { // Cache entry entry.insert(builtin).as_ref() }; } // Try all permutations with `Dynamic` wildcards hash = calc_fn_hash_full( hash_base, args.as_ref() .expect("no permutations") .iter() .enumerate() .map(|(i, a)| { let mask = 1usize << (num_args - i - 1); if bitmask & mask == 0 { a.type_id() } else { // Replace with `Dynamic` TypeId::of::() } }), ); bitmask += 1; } } } } /// # Main Entry-Point (Native by Name) /// /// Call a native Rust function registered with the [`Engine`] by name. /// /// # WARNING /// /// Function call arguments 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 unless for the first `&mut` argument - /// all others are silently replaced by `()`! pub(crate) fn exec_native_fn_call( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, name: &str, op_token: Option, hash: u64, args: &mut FnCallArgs, is_ref_mut: bool, pos: Position, ) -> RhaiResultOf<(Dynamic, bool)> { self.track_operation(global, pos)?; // Check if function access already in the cache let local_entry = &mut None; let func = self.resolve_fn( global, caches, local_entry, op_token, hash, Some(args), true, ); if let Some(FnResolutionCacheEntry { func, source }) = func { assert!(func.is_native()); // Push a new call stack frame #[cfg(feature = "debugging")] let orig_call_stack_len = global .debugger .as_ref() .map_or(0, |dbg| dbg.call_stack().len()); let backup = &mut ArgBackup::new(); // Calling pure function but the first argument is a reference? let swap = is_ref_mut && func.is_pure() && !args.is_empty(); if swap { // Clone the first argument backup.change_first_arg_to_copy(args); } #[cfg(feature = "debugging")] if self.is_debugger_registered() { let source = source.clone().or_else(|| global.source.clone()); global.debugger_mut().push_call_stack_frame( self.get_interned_string(name), args.iter().map(|v| (*v).clone()).collect(), source, pos, ); } // Run external function let is_method = func.is_method(); let src = source.as_ref().map(|s| s.as_str()); let context = if func.has_context() { Some((self, name, src, &*global, pos).into()) } else { None }; let mut _result = if let Some(f) = func.get_plugin_fn() { if !f.is_pure() && !args.is_empty() && args[0].is_read_only() { Err(ERR::ErrorNonPureMethodCallOnConstant(name.to_string(), pos).into()) } else { f.call(context, args) } } else if let Some(f) = func.get_native_fn() { f(context, args) } else { unreachable!(); } .and_then(|r| self.check_data_size(r, pos)) .map_err(|err| err.fill_position(pos)); if swap { backup.restore_first_arg(args); } #[cfg(feature = "debugging")] if self.is_debugger_registered() { use crate::eval::{DebuggerEvent, DebuggerStatus}; let trigger = match global.debugger().status { DebuggerStatus::FunctionExit(n) => n >= global.level, DebuggerStatus::Next(.., true) => true, _ => false, }; if trigger { let scope = &mut Scope::new(); let node = crate::ast::Stmt::Noop(pos); let node = (&node).into(); let event = match _result { Ok(ref r) => DebuggerEvent::FunctionExitWithValue(r), Err(ref err) => DebuggerEvent::FunctionExitWithError(err), }; match self.run_debugger_raw(global, caches, scope, None, node, event) { Ok(..) => (), Err(err) => _result = Err(err), } } // Pop the call stack global.debugger_mut().rewind_call_stack(orig_call_stack_len); } let result = _result?; // Check the data size of any `&mut` object, which may be changed. #[cfg(not(feature = "unchecked"))] if is_ref_mut && !args.is_empty() { self.check_data_size(&*args[0], pos)?; } // See if the function match print/debug (which requires special processing) return Ok(match name { KEYWORD_PRINT => { let text = result.into_immutable_string().map_err(|typ| { let t = self.map_type_name(type_name::()).into(); ERR::ErrorMismatchOutputType(t, typ.into(), pos) })?; ((*self.print)(&text).into(), false) } KEYWORD_DEBUG => { let text = result.into_immutable_string().map_err(|typ| { let t = self.map_type_name(type_name::()).into(); ERR::ErrorMismatchOutputType(t, typ.into(), pos) })?; ((*self.debug)(&text, global.source(), pos).into(), false) } _ => (result, is_method), }); } // Error handling match name { // index getter function not found? #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] crate::engine::FN_IDX_GET => { assert!(args.len() == 2); let t0 = self.map_type_name(args[0].type_name()); let t1 = self.map_type_name(args[1].type_name()); Err(ERR::ErrorIndexingType(format!("{t0} [{t1}]"), pos).into()) } // index setter function not found? #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] crate::engine::FN_IDX_SET => { assert!(args.len() == 3); let t0 = self.map_type_name(args[0].type_name()); let t1 = self.map_type_name(args[1].type_name()); let t2 = self.map_type_name(args[2].type_name()); Err(ERR::ErrorIndexingType(format!("{t0} [{t1}] = {t2}"), pos).into()) } // Getter function not found? #[cfg(not(feature = "no_object"))] _ if name.starts_with(crate::engine::FN_GET) => { assert!(args.len() == 1); let prop = &name[crate::engine::FN_GET.len()..]; let t0 = self.map_type_name(args[0].type_name()); Err(ERR::ErrorDotExpr( format!( "Unknown property '{prop}' - a getter is not registered for type '{t0}'" ), pos, ) .into()) } // Setter function not found? #[cfg(not(feature = "no_object"))] _ if name.starts_with(crate::engine::FN_SET) => { assert!(args.len() == 2); let prop = &name[crate::engine::FN_SET.len()..]; let t0 = self.map_type_name(args[0].type_name()); let t1 = self.map_type_name(args[1].type_name()); Err(ERR::ErrorDotExpr( format!( "No writable property '{prop}' - a setter is not registered for type '{t0}' to handle '{t1}'" ), pos, ) .into()) } // Raise error _ => { Err(ERR::ErrorFunctionNotFound(self.gen_fn_call_signature(name, args), pos).into()) } } } /// # Main Entry-Point (By Name) /// /// Perform an actual function call, native Rust or scripted, by name, taking care of special functions. /// /// # 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 unless for the first `&mut` argument - /// all others are silently replaced by `()`! pub(crate) fn exec_fn_call( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, _scope: Option<&mut Scope>, fn_name: &str, op_token: Option, hashes: FnCallHashes, mut _args: &mut FnCallArgs, is_ref_mut: bool, _is_method_call: bool, pos: Position, ) -> RhaiResultOf<(Dynamic, bool)> { // Check for data race. #[cfg(not(feature = "no_closure"))] ensure_no_data_race(fn_name, _args, is_ref_mut)?; auto_restore! { let orig_level = global.level; global.level += 1 } // These may be redirected from method style calls. if hashes.is_native_only() { match fn_name { // Handle type_of() KEYWORD_TYPE_OF if _args.len() == 1 => { let typ = self.map_type_name(_args[0].type_name()).to_string().into(); return Ok((typ, false)); } // Handle is_def_fn() #[cfg(not(feature = "no_function"))] crate::engine::KEYWORD_IS_DEF_FN if _args.len() == 2 && _args[0].is_fnptr() && _args[1].is_int() => { let fn_name = _args[0].read_lock::().expect("`FnPtr`"); let num_params = _args[1].as_int().expect("`INT`"); return Ok(( if (0..=crate::MAX_USIZE_INT).contains(&num_params) { #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] let hash_script = calc_fn_hash(None, fn_name.as_str(), num_params as usize); self.has_script_fn(global, caches, hash_script) } else { false } .into(), false, )); } // Handle is_shared() #[cfg(not(feature = "no_closure"))] crate::engine::KEYWORD_IS_SHARED => { unreachable!("{} called as method", fn_name) } KEYWORD_FN_PTR | KEYWORD_EVAL | KEYWORD_IS_DEF_VAR | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY => { unreachable!("{} called as method", fn_name) } _ => (), } } #[cfg(not(feature = "no_function"))] if !hashes.is_native_only() { // Script-defined function call? let hash = hashes.script(); let local_entry = &mut None; if let Some(FnResolutionCacheEntry { func, ref source }) = self .resolve_fn(global, caches, local_entry, None, hash, None, false) .cloned() { // Script function call assert!(func.is_script()); let f = func.get_script_fn_def().expect("script-defined function"); let environ = func.get_encapsulated_environ(); if f.body.is_empty() { return Ok((Dynamic::UNIT, false)); } let mut empty_scope; let scope = if let Some(scope) = _scope { scope } else { empty_scope = Scope::new(); &mut empty_scope }; let orig_source = mem::replace(&mut global.source, source.clone()); auto_restore!(global => move |g| g.source = orig_source); return if _is_method_call { // Method call of script function - map first argument to `this` let (first_arg, rest_args) = _args.split_first_mut().unwrap(); self.call_script_fn( global, caches, scope, Some(first_arg), environ, f, rest_args, true, pos, ) } else { // Normal call of script function let backup = &mut ArgBackup::new(); // The first argument is a reference? let swap = is_ref_mut && !_args.is_empty(); if swap { backup.change_first_arg_to_copy(_args); } auto_restore!(args = (_args) if swap => move |a| backup.restore_first_arg(a)); self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos) } .map(|r| (r, false)); } } // Native function call let hash = hashes.native(); self.exec_native_fn_call( global, caches, fn_name, op_token, hash, _args, is_ref_mut, pos, ) } /// Evaluate an argument. #[inline] pub(crate) fn get_arg_value( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, scope: &mut Scope, this_ptr: Option<&mut Dynamic>, arg_expr: &Expr, ) -> RhaiResultOf<(Dynamic, Position)> { // Literal values if let Some(value) = arg_expr.get_literal_value() { self.track_operation(global, arg_expr.start_position())?; #[cfg(feature = "debugging")] self.run_debugger(global, caches, scope, this_ptr, arg_expr)?; return Ok((value, arg_expr.start_position())); } // Do not match function exit for arguments #[cfg(feature = "debugging")] let reset = global.debugger.as_deref_mut().and_then(|dbg| { dbg.clear_status_if(|status| { matches!(status, crate::eval::DebuggerStatus::FunctionExit(..)) }) }); #[cfg(feature = "debugging")] auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); self.eval_expr(global, caches, scope, this_ptr, arg_expr) .map(|r| (r, arg_expr.start_position())) } /// Call a dot method. #[cfg(not(feature = "no_object"))] pub(crate) fn make_method_call( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, fn_name: &str, mut hash: FnCallHashes, target: &mut crate::eval::Target, mut call_args: &mut [Dynamic], first_arg_pos: Position, fn_call_pos: Position, ) -> RhaiResultOf<(Dynamic, bool)> { let is_ref_mut = target.is_ref(); let (result, updated) = match fn_name { // Handle fn_ptr.call(...) KEYWORD_FN_PTR_CALL if target.is_fnptr() => { let fn_ptr = target.read_lock::().expect("`FnPtr`"); // Arguments are passed as-is, adding the curried arguments let mut curry = FnArgsVec::with_capacity(fn_ptr.curry().len()); curry.extend(fn_ptr.curry().iter().cloned()); let args = &mut FnArgsVec::with_capacity(curry.len() + call_args.len()); args.extend(curry.iter_mut()); args.extend(call_args.iter_mut()); // Linked to scripted function? #[cfg(not(feature = "no_function"))] if let Some(fn_def) = fn_ptr.fn_def() { if fn_def.params.len() == args.len() { return self .call_script_fn( global, caches, &mut Scope::new(), None, fn_ptr.encapsulated_environ(), fn_def, args, true, fn_call_pos, ) .map(|v| (v, false)); } } #[cfg(not(feature = "no_function"))] let is_anon = fn_ptr.is_anonymous(); #[cfg(feature = "no_function")] let is_anon = false; // Redirect function name let fn_name = fn_ptr.fn_name(); // Recalculate hashes let new_hash = if !is_anon && !is_valid_function_name(fn_name) { FnCallHashes::from_native(calc_fn_hash(None, fn_name, args.len())) } else { calc_fn_hash(None, fn_name, args.len()).into() }; // Map it to name(args) in function-call style self.exec_fn_call( global, caches, None, fn_name, None, new_hash, args, false, false, fn_call_pos, ) } // Handle obj.call(fn_ptr, ...) KEYWORD_FN_PTR_CALL => { if call_args.is_empty() { let typ = self.map_type_name(target.type_name()); return Err(self.make_type_mismatch_err::(typ, fn_call_pos)); } else if !call_args[0].is_fnptr() { let typ = self.map_type_name(call_args[0].type_name()); return Err(self.make_type_mismatch_err::(typ, first_arg_pos)); } // FnPtr call on object let fn_ptr = mem::take(&mut call_args[0]).cast::(); #[cfg(not(feature = "no_function"))] let (fn_name, is_anon, fn_curry, _environ, fn_def) = { let is_anon = fn_ptr.is_anonymous(); let (fn_name, fn_curry, environ, fn_def) = fn_ptr.take_data(); (fn_name, is_anon, fn_curry, environ, fn_def) }; #[cfg(feature = "no_function")] let (fn_name, is_anon, fn_curry, _environ) = { let (fn_name, fn_curry, environ) = fn_ptr.take_data(); (fn_name, false, fn_curry, environ) }; // Replace the first argument with the object pointer, adding the curried arguments call_args = &mut call_args[1..]; let mut curry = FnArgsVec::with_capacity(fn_curry.len()); curry.extend(fn_curry.into_iter()); let args = &mut FnArgsVec::with_capacity(curry.len() + call_args.len() + 1); args.extend(curry.iter_mut()); args.extend(call_args.iter_mut()); // Linked to scripted function? #[cfg(not(feature = "no_function"))] if let Some(fn_def) = fn_def { if fn_def.params.len() == args.len() { // Check for data race. #[cfg(not(feature = "no_closure"))] ensure_no_data_race(&fn_def.name, args, false)?; return self .call_script_fn( global, caches, &mut Scope::new(), Some(target), _environ.as_deref(), &fn_def, args, true, fn_call_pos, ) .map(|v| (v, false)); } } // Add the first argument with the object pointer args.insert(0, target.as_mut()); // Recalculate hash let new_hash = if !is_anon && !is_valid_function_name(&fn_name) { FnCallHashes::from_native(calc_fn_hash(None, &fn_name, args.len())) } else { FnCallHashes::from_all( #[cfg(not(feature = "no_function"))] calc_fn_hash(None, &fn_name, args.len() - 1), calc_fn_hash(None, &fn_name, args.len()), ) }; // Map it to name(args) in function-call style self.exec_fn_call( global, caches, None, &fn_name, None, new_hash, args, is_ref_mut, true, fn_call_pos, ) } KEYWORD_FN_PTR_CURRY => { if !target.is_fnptr() { let typ = self.map_type_name(target.type_name()); return Err(self.make_type_mismatch_err::(typ, fn_call_pos)); } let mut fn_ptr = target.read_lock::().expect("`FnPtr`").clone(); // Append the new curried arguments to the existing list. call_args.iter_mut().map(mem::take).for_each(|value| { fn_ptr.add_curry(value); }); Ok((fn_ptr.into(), false)) } // Handle is_shared() #[cfg(not(feature = "no_closure"))] crate::engine::KEYWORD_IS_SHARED if call_args.is_empty() => { return Ok((target.is_shared().into(), false)); } _ => { let mut fn_name = fn_name; let _redirected; let mut _arg_values: FnArgsVec<_>; let mut call_args = call_args; // Check if it is a map method call in OOP style #[cfg(not(feature = "no_object"))] if let Some(map) = target.read_lock::() { if let Some(val) = map.get(fn_name) { if let Some(fn_ptr) = val.read_lock::() { #[cfg(not(feature = "no_function"))] let is_anon = fn_ptr.is_anonymous(); #[cfg(feature = "no_function")] let is_anon = false; // Remap the function name _redirected = fn_ptr.fn_name_raw().clone(); fn_name = &_redirected; // Add curried arguments if fn_ptr.is_curried() { _arg_values = fn_ptr .curry() .iter() .cloned() .chain(call_args.iter_mut().map(mem::take)) .collect(); call_args = &mut _arg_values; } // Recalculate the hash based on the new function name and new arguments hash = if !is_anon && !is_valid_function_name(fn_name) { FnCallHashes::from_native(calc_fn_hash( None, fn_name, call_args.len() + 1, )) } else { FnCallHashes::from_all( #[cfg(not(feature = "no_function"))] calc_fn_hash(None, fn_name, call_args.len()), calc_fn_hash(None, fn_name, call_args.len() + 1), ) }; } } }; // Attached object pointer in front of the arguments let mut args = FnArgsVec::with_capacity(call_args.len() + 1); args.push(target.as_mut()); args.extend(call_args.iter_mut()); self.exec_fn_call( global, caches, None, fn_name, None, hash, &mut args, is_ref_mut, true, fn_call_pos, ) } }?; // Propagate the changed value back to the source if necessary if updated { target.propagate_changed_value(fn_call_pos)?; } Ok((result, updated)) } /// Call a function in normal function-call style. pub(crate) fn make_function_call( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, scope: &mut Scope, mut this_ptr: Option<&mut Dynamic>, fn_name: &str, op_token: Option, first_arg: Option<&Expr>, args_expr: &[Expr], hashes: FnCallHashes, capture_scope: bool, pos: Position, ) -> RhaiResult { let mut first_arg = first_arg; let mut a_expr = args_expr; let mut total_args = usize::from(first_arg.is_some()) + a_expr.len(); let mut curry = FnArgsVec::new_const(); let mut name = fn_name; let mut hashes = hashes; let redirected; // Handle call() - Redirect function call match name { _ if op_token.is_some() => (), // Handle call(fn_ptr, ...) KEYWORD_FN_PTR_CALL if total_args >= 1 => { let arg = first_arg.unwrap(); let (arg_value, arg_pos) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), arg)?; if !arg_value.is_fnptr() { let typ = self.map_type_name(arg_value.type_name()); return Err(self.make_type_mismatch_err::(typ, arg_pos)); } let fn_ptr = arg_value.cast::(); #[cfg(not(feature = "no_function"))] let (fn_name, is_anon, fn_curry, _environ, fn_def) = { let is_anon = fn_ptr.is_anonymous(); let (fn_name, fn_curry, environ, fn_def) = fn_ptr.take_data(); (fn_name, is_anon, fn_curry, environ, fn_def) }; #[cfg(feature = "no_function")] let (fn_name, is_anon, fn_curry, _environ) = { let (fn_name, fn_curry, environ) = fn_ptr.take_data(); (fn_name, false, fn_curry, environ) }; curry.extend(fn_curry.into_iter()); // Linked to scripted function? #[cfg(not(feature = "no_function"))] if let Some(fn_def) = fn_def { if fn_def.params.len() == curry.len() + a_expr.len() { // Evaluate arguments let mut arg_values = curry .into_iter() .map(Ok) .chain(a_expr.iter().map(|expr| -> Result<_, RhaiError> { let this_ptr = this_ptr.as_deref_mut(); self.get_arg_value(global, caches, scope, this_ptr, expr) .map(|(v, ..)| v) })) .collect::>>()?; let args = &mut arg_values.iter_mut().collect::>(); let scope = &mut Scope::new(); let environ = _environ.as_deref(); return self.call_script_fn( global, caches, scope, None, environ, &fn_def, args, true, pos, ); } } // Redirect function name redirected = fn_name; name = &redirected; // Shift the arguments first_arg = a_expr.get(0); if !a_expr.is_empty() { a_expr = &a_expr[1..]; } total_args -= 1; // Recalculate hash let args_len = total_args + curry.len(); hashes = if !is_anon && !is_valid_function_name(name) { FnCallHashes::from_native(calc_fn_hash(None, name, args_len)) } else { calc_fn_hash(None, name, args_len).into() }; } // Handle Fn() KEYWORD_FN_PTR if total_args == 1 => { let arg = first_arg.unwrap(); let (arg_value, arg_pos) = self.get_arg_value(global, caches, scope, this_ptr, arg)?; // Fn - only in function call style return arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos)) .and_then(FnPtr::try_from) .map(Into::into) .map_err(|err| err.fill_position(arg_pos)); } // Handle curry() KEYWORD_FN_PTR_CURRY if total_args > 1 => { let first = first_arg.unwrap(); let (arg_value, arg_pos) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), first)?; if !arg_value.is_fnptr() { let typ = self.map_type_name(arg_value.type_name()); return Err(self.make_type_mismatch_err::(typ, arg_pos)); } let mut fn_ptr = arg_value.cast::(); // Append the new curried arguments to the existing list. for expr in a_expr { let (value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; fn_ptr.add_curry(value); } return Ok(fn_ptr.into()); } // Handle is_shared() #[cfg(not(feature = "no_closure"))] crate::engine::KEYWORD_IS_SHARED if total_args == 1 => { let arg = first_arg.unwrap(); let (arg_value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), arg)?; return Ok(arg_value.is_shared().into()); } // Handle is_def_fn() #[cfg(not(feature = "no_function"))] crate::engine::KEYWORD_IS_DEF_FN if total_args == 2 => { let first = first_arg.unwrap(); let (arg_value, arg_pos) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), first)?; let fn_name = arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; let (arg_value, arg_pos) = self.get_arg_value(global, caches, scope, this_ptr, &a_expr[0])?; let num_params = arg_value .as_int() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; return Ok(if (0..=crate::MAX_USIZE_INT).contains(&num_params) { #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] let hash_script = calc_fn_hash(None, &fn_name, num_params as usize); self.has_script_fn(global, caches, hash_script) } else { false } .into()); } // Handle is_def_var() KEYWORD_IS_DEF_VAR if total_args == 1 => { let arg = first_arg.unwrap(); let (arg_value, arg_pos) = self.get_arg_value(global, caches, scope, this_ptr, arg)?; let var_name = arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; return Ok(scope.contains(&var_name).into()); } // Handle eval() KEYWORD_EVAL if total_args == 1 => { // eval - only in function call style let orig_scope_len = scope.len(); #[cfg(not(feature = "no_module"))] let orig_imports_len = global.num_imports(); let arg = first_arg.unwrap(); let (arg_value, pos) = self.get_arg_value(global, caches, scope, this_ptr, arg)?; let s = &arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; let orig_level = global.level; global.level += 1; let result = self.eval_script_expr_in_place(global, caches, scope, s, pos); // IMPORTANT! If the eval defines new variables in the current scope, // all variable offsets from this point on will be mis-aligned. // The same is true for imports. let scope_changed = scope.len() != orig_scope_len; #[cfg(not(feature = "no_module"))] let scope_changed = scope_changed || global.num_imports() != orig_imports_len; if scope_changed { global.always_search_scope = true; } global.level = orig_level; return result.map_err(|err| { ERR::ErrorInFunctionCall( KEYWORD_EVAL.to_string(), global.source().unwrap_or("").to_string(), err, pos, ) .into() }); } _ => (), } // Normal function call - except for Fn, curry, call and eval (handled above) let mut arg_values = FnArgsVec::with_capacity(total_args); let mut args = FnArgsVec::with_capacity(total_args + curry.len()); let mut is_ref_mut = false; // Capture parent scope? // // If so, do it separately because we cannot convert the first argument (if it is a simple // variable access) to &mut because `scope` is needed. if capture_scope && !scope.is_empty() { for expr in first_arg.iter().copied().chain(a_expr.iter()) { let (value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; arg_values.push(value.flatten()); } args.extend(curry.iter_mut()); args.extend(arg_values.iter_mut()); // Use parent scope let scope = Some(scope); return self .exec_fn_call( global, caches, scope, name, op_token, hashes, &mut args, is_ref_mut, false, pos, ) .map(|(v, ..)| v); } // Call with blank scope if total_args == 0 && curry.is_empty() { // No arguments } else { // If the first argument is a variable, and there is no curried arguments, // convert to method-call style in order to leverage potential &mut first argument and // avoid cloning the value if curry.is_empty() && first_arg.map_or(false, |expr| expr.is_variable_access(false)) { let first_expr = first_arg.unwrap(); #[cfg(feature = "debugging")] self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), first_expr)?; // func(x, ...) -> x.func(...) for expr in a_expr { let (value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; arg_values.push(value.flatten()); } let mut target = self.search_namespace(global, caches, scope, this_ptr, first_expr)?; if target.is_read_only() { target = target.into_owned(); } self.track_operation(global, first_expr.position())?; if target.is_shared() || target.is_temp_value() { arg_values.insert(0, target.take_or_clone().flatten()); } else { // Turn it into a method call only if the object is not shared and not a simple value is_ref_mut = true; let obj_ref = target.take_ref().expect("ref"); args.push(obj_ref); } } else { // func(..., ...) for expr in first_arg.into_iter().chain(a_expr.iter()) { let (value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; arg_values.push(value.flatten()); } args.extend(curry.iter_mut()); } args.extend(arg_values.iter_mut()); } self.exec_fn_call( global, caches, None, name, op_token, hashes, &mut args, is_ref_mut, false, pos, ) .map(|(v, ..)| v) } /// Call a namespace-qualified function in normal function-call style. #[cfg(not(feature = "no_module"))] pub(crate) fn make_qualified_function_call( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, scope: &mut Scope, mut this_ptr: Option<&mut Dynamic>, namespace: &crate::ast::Namespace, fn_name: &str, args_expr: &[Expr], hash: u64, pos: Position, ) -> RhaiResult { let mut arg_values = FnArgsVec::with_capacity(args_expr.len()); let args = &mut FnArgsVec::with_capacity(args_expr.len()); let mut first_arg_value = None; if args_expr.is_empty() { // No arguments } else { // See if the first argument is a variable (not namespace-qualified). // If so, convert to method-call style in order to leverage potential &mut first argument // and avoid cloning the value if !args_expr.is_empty() && args_expr[0].is_variable_access(true) { #[cfg(feature = "debugging")] self.run_debugger( global, caches, scope, this_ptr.as_deref_mut(), &args_expr[0], )?; // func(x, ...) -> x.func(...) arg_values.push(Dynamic::UNIT); for expr in args_expr.iter().skip(1) { let (value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; arg_values.push(value.flatten()); } // Get target reference to first argument let first_arg = &args_expr[0]; let target = self.search_scope_only(global, caches, scope, this_ptr, first_arg)?; self.track_operation(global, first_arg.position())?; #[cfg(not(feature = "no_closure"))] let target_is_shared = target.is_shared(); #[cfg(feature = "no_closure")] let target_is_shared = false; if target_is_shared || target.is_temp_value() { arg_values[0] = target.take_or_clone().flatten(); args.extend(arg_values.iter_mut()); } else { // Turn it into a method call only if the object is not shared and not a simple value let (first, rest) = arg_values.split_first_mut().unwrap(); first_arg_value = Some(first); let obj_ref = target.take_ref().expect("ref"); args.push(obj_ref); args.extend(rest.iter_mut()); } } else { // func(..., ...) or func(mod::x, ...) for expr in args_expr { let (value, ..) = self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; arg_values.push(value.flatten()); } args.extend(arg_values.iter_mut()); } } // Search for the root namespace let module = self .search_imports(global, namespace) .ok_or_else(|| ERR::ErrorModuleNotFound(namespace.to_string(), namespace.position()))?; // First search script-defined functions in namespace (can override built-in) let mut func = match module.get_qualified_fn(hash) { // Then search native Rust functions None => { self.track_operation(global, pos)?; let hash_qualified_fn = calc_fn_hash_full(hash, args.iter().map(|a| a.type_id())); module.get_qualified_fn(hash_qualified_fn) } r => r, }; // Check for `Dynamic` parameters. // // Note - This is done during every function call mismatch without cache, // so hopefully the number of arguments should not be too many // (expected because closures cannot be qualified). if func.is_none() && !args.is_empty() { let num_args = args.len(); let max_bitmask = 1usize << usize::min(num_args, MAX_DYNAMIC_PARAMETERS); let mut bitmask = 1usize; // Bitmask of which parameter to replace with `Dynamic` // Try all permutations with `Dynamic` wildcards while bitmask < max_bitmask { let hash_qualified_fn = calc_fn_hash_full( hash, args.iter().enumerate().map(|(i, a)| { let mask = 1usize << (num_args - i - 1); if bitmask & mask == 0 { a.type_id() } else { // Replace with `Dynamic` TypeId::of::() } }), ); self.track_operation(global, pos)?; if let Some(f) = module.get_qualified_fn(hash_qualified_fn) { func = Some(f); break; } bitmask += 1; } } // Clone first argument if the function is not a method after-all if !func.map_or(true, CallableFunction::is_method) { if let Some(first) = first_arg_value { *first = args[0].clone(); args[0] = first; } } auto_restore! { let orig_level = global.level; global.level += 1 } match func { #[cfg(not(feature = "no_function"))] Some(func) if func.is_script() => { let f = func.get_script_fn_def().expect("script-defined function"); let environ = func.get_encapsulated_environ(); let scope = &mut Scope::new(); let orig_source = mem::replace(&mut global.source, module.id_raw().cloned()); auto_restore!(global => move |g| g.source = orig_source); self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos) } Some(f) if f.is_plugin_fn() => { let f = f.get_plugin_fn().expect("plugin function"); let context = if f.has_context() { Some((self, fn_name, module.id(), &*global, pos).into()) } else { None }; if !f.is_pure() && !args.is_empty() && args[0].is_read_only() { Err(ERR::ErrorNonPureMethodCallOnConstant(fn_name.to_string(), pos).into()) } else { f.call(context, args) .and_then(|r| self.check_data_size(r, pos)) } } Some(f) if f.is_native() => { let func = f.get_native_fn().expect("native function"); let context = if f.has_context() { Some((self, fn_name, module.id(), &*global, pos).into()) } else { None }; func(context, args).and_then(|r| self.check_data_size(r, pos)) } Some(f) => unreachable!("unknown function type: {:?}", f), None => { let sig = if namespace.is_empty() { self.gen_fn_call_signature(fn_name, args) } else { format!( "{namespace}{}{}", crate::tokenizer::Token::DoubleColon.literal_syntax(), self.gen_fn_call_signature(fn_name, args) ) }; Err(ERR::ErrorFunctionNotFound(sig, pos).into()) } } } /// Evaluate a text script in place - used primarily for 'eval'. pub(crate) fn eval_script_expr_in_place( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, scope: &mut Scope, script: &str, _pos: Position, ) -> RhaiResult { self.track_operation(global, _pos)?; let script = script.trim(); if script.is_empty() { return Ok(Dynamic::UNIT); } // Compile the script text // No optimizations because we only run it once let ast = self.compile_with_scope_and_optimization_level( None, [script], #[cfg(not(feature = "no_optimize"))] OptimizationLevel::None, #[cfg(feature = "no_optimize")] OptimizationLevel::default(), )?; // If new functions are defined within the eval string, it is an error #[cfg(not(feature = "no_function"))] if ast.has_functions() { return Err(crate::PERR::WrongFnDefinition.into()); } let statements = ast.statements(); if statements.is_empty() { return Ok(Dynamic::UNIT); } // Evaluate the AST self.eval_global_statements(global, caches, scope, statements) } /// # Main Entry-Point (`FnCallExpr`) /// /// Evaluate a function call expression. pub(crate) fn eval_fn_call_expr( &self, global: &mut GlobalRuntimeState, caches: &mut Caches, scope: &mut Scope, mut this_ptr: Option<&mut Dynamic>, expr: &FnCallExpr, pos: Position, ) -> RhaiResult { let FnCallExpr { #[cfg(not(feature = "no_module"))] namespace, name, hashes, args, op_token, capture_parent_scope: capture, .. } = expr; let op_token = op_token.clone(); // Short-circuit native unary operator call if under Fast Operators mode if op_token == Some(Token::Bang) && self.fast_operators() && args.len() == 1 { let mut value = self .get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), &args[0])? .0 .flatten(); return value.as_bool().map(|r| (!r).into()).or_else(|_| { let operand = &mut [&mut value]; self.exec_fn_call( global, caches, None, name, op_token, *hashes, operand, false, false, pos, ) .map(|(v, ..)| v) }); } // Short-circuit native binary operator call if under Fast Operators mode if op_token.is_some() && self.fast_operators() && args.len() == 2 { let mut lhs = self .get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), &args[0])? .0 .flatten(); let mut rhs = self .get_arg_value(global, caches, scope, this_ptr, &args[1])? .0 .flatten(); let operands = &mut [&mut lhs, &mut rhs]; if let Some((func, need_context)) = get_builtin_binary_op_fn(op_token.clone().unwrap(), operands[0], operands[1]) { // Built-in found auto_restore! { let orig_level = global.level; global.level += 1 } let context = if need_context { Some((self, name.as_str(), None, &*global, pos).into()) } else { None }; return func(context, operands); } return self .exec_fn_call( global, caches, None, name, op_token, *hashes, operands, false, false, pos, ) .map(|(v, ..)| v); } #[cfg(not(feature = "no_module"))] if !namespace.is_empty() { // Qualified function call let hash = hashes.native(); return self.make_qualified_function_call( global, caches, scope, this_ptr, namespace, name, args, hash, pos, ); } // Normal function call let (first_arg, rest_args) = args.split_first().map_or_else( || (None, args.as_ref()), |(first, rest)| (Some(first), rest), ); self.make_function_call( global, caches, scope, this_ptr, name, op_token, first_arg, rest_args, *hashes, *capture, pos, ) } }