diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ee4fa9..228ee0d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,18 @@ Rhai Release Notes Version 1.10.0 ============== -This version, by default, turns on _Fast Operators_ mode, which assumes that built-in operators for -standard data types are never overloaded – in the vast majority of cases this should be so. -Avoid checking for overloads may result in substantial speed improvements especially for -operator-heavy scripts. +This version introduces _Fast Operators_ mode, which is turned on by default but can be disabled via +a new options API: `Engine::set_fast_operators`. + +_Fast Operators_ mode assumes that none of Rhai's built-in operators for standard data types are +overloaded by user-registered functions. In the vast majority of cases this should be so (really, +who overloads the `+` operator for integers anyway?). + +This assumption allows the `Engine` to avoid checking for overloads for every single operator call. +This usually results in substantial speed improvements, especially for expressions. + +Minimum Rust Version +-------------------- The minimum Rust version is now `1.61.0` in order to use some `const` generics. @@ -28,7 +36,7 @@ New features ### Fast operators -* A new option `Engine::fast_operators` is introduced (default to `true`) that short-circuits all built-in operators of built-in types for higher speed. User overloads are ignored. For operator-heavy scripts, this may yield substantial speed-up's. +* A new option `Engine::fast_operators` is introduced (default to `true`) to enable/disable _Fast Operators_ mode. ### Fallible type iterators diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 56bb83b2..82f5745a 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -5,14 +5,14 @@ use crate::ast::{Expr, FnCallExpr, OpAssignment}; use crate::engine::{KEYWORD_THIS, OP_CONCAT}; use crate::eval::FnResolutionCacheEntry; use crate::func::{ - calc_fn_params_hash, combine_hashes, get_builtin_binary_op_fn, CallableFunction, FnAny, + calc_fn_params_hash, combine_hashes, gen_fn_call_signature, get_builtin_binary_op_fn, + CallableFunction, FnAny, }; use crate::types::dynamic::AccessMode; use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, ERR}; -use std::collections::btree_map::Entry; -use std::num::NonZeroUsize; #[cfg(feature = "no_std")] use std::prelude::v1::*; +use std::{collections::btree_map::Entry, num::NonZeroUsize}; impl Engine { /// Search for a module within an imports stack. @@ -223,84 +223,78 @@ impl Engine { level: usize, ) -> RhaiResult { let FnCallExpr { - name, - #[cfg(not(feature = "no_module"))] - namespace, - capture_parent_scope: capture, - is_native_operator: native_ops, - hashes, - args, - .. + name, hashes, args, .. } = expr; // Short-circuit native binary operator call if under Fast Operators mode - if *native_ops && self.fast_operators() && args.len() == 2 { + if expr.is_native_operator && self.fast_operators() && (args.len() == 1 || args.len() == 2) + { let mut lhs = self .get_arg_value(scope, global, caches, lib, this_ptr, &args[0], level)? .0 .flatten(); - let mut rhs = self - .get_arg_value(scope, global, caches, lib, this_ptr, &args[1], level)? - .0 - .flatten(); - - let args = &mut [&mut lhs, &mut rhs]; - - let hash = combine_hashes( - hashes.native, - calc_fn_params_hash(args.iter().map(|a| a.type_id())), - ); - - let c = caches.fn_resolution_cache_mut(); - - let entry = if let Entry::Vacant(e) = c.entry(hash) { - match get_builtin_binary_op_fn(&name, args[0], args[1]) { - Some(f) => { - let entry = FnResolutionCacheEntry { - func: CallableFunction::from_method(Box::new(f) as Box), - source: None, - }; - e.insert(Some(entry)); - c.get(&hash).unwrap().as_ref().unwrap() - } - None => { - return self - .exec_fn_call( - None, global, caches, lib, name, *hashes, args, false, false, pos, - level, - ) - .map(|(v, ..)| v) - } - } + let mut rhs = if args.len() == 2 { + self.get_arg_value(scope, global, caches, lib, this_ptr, &args[1], level)? + .0 + .flatten() } else { - match c.get(&hash).unwrap() { - Some(entry) => entry, - None => { - return Err(ERR::ErrorFunctionNotFound( - self.gen_fn_call_signature( - #[cfg(not(feature = "no_module"))] - &crate::ast::Namespace::NONE, - name, - args, - ), - pos, - ) - .into()) - } - } + Dynamic::UNIT + }; + + let mut operands = [&mut lhs, &mut rhs]; + let operands = if args.len() == 2 { + &mut operands[..] + } else { + &mut operands[0..1] + }; + + let hash = calc_fn_params_hash(operands.iter().map(|a| a.type_id())); + let hash = combine_hashes(hashes.native, hash); + + let cache = caches.fn_resolution_cache_mut(); + + let func = if let Entry::Vacant(entry) = cache.entry(hash) { + let func = if args.len() == 2 { + get_builtin_binary_op_fn(&name, operands[0], operands[1]) + } else { + None + }; + + if let Some(f) = func { + entry.insert(Some(FnResolutionCacheEntry { + func: CallableFunction::from_method(Box::new(f) as Box), + source: None, + })); + &cache.get(&hash).unwrap().as_ref().unwrap().func + } else { + let result = self.exec_fn_call( + None, global, caches, lib, name, *hashes, operands, false, false, pos, + level, + ); + return result.map(|(v, ..)| v); + } + } else if let Some(entry) = cache.get(&hash).unwrap() { + &entry.func + } else { + let sig = gen_fn_call_signature(self, name, operands); + return Err(ERR::ErrorFunctionNotFound(sig, pos).into()); }; - let func = entry.func.get_native_fn().unwrap(); let context = (self, name, None, &*global, lib, pos, level).into(); - let result = (func)(context, args); + let result = if func.is_plugin_fn() { + func.get_plugin_fn().unwrap().call(context, operands) + } else { + func.get_native_fn().unwrap()(context, operands) + }; return self.check_return_value(result, pos); } #[cfg(not(feature = "no_module"))] - if !namespace.is_empty() { + if !expr.namespace.is_empty() { // Qualified function call let hash = hashes.native; + let namespace = &expr.namespace; return self.make_qualified_function_call( scope, global, caches, lib, this_ptr, namespace, name, args, hash, pos, level, @@ -323,8 +317,8 @@ impl Engine { first_arg, args, *hashes, - *capture, - *native_ops, + expr.capture_parent_scope, + expr.is_native_operator, pos, level, ) diff --git a/src/func/call.rs b/src/func/call.rs index 93e808ed..8829fab7 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -127,41 +127,48 @@ pub fn ensure_no_data_race( Ok(()) } -impl Engine { - /// Generate the signature for a function call. - #[inline] - #[must_use] - pub(crate) fn gen_fn_call_signature( - &self, - #[cfg(not(feature = "no_module"))] namespace: &crate::ast::Namespace, - fn_name: &str, - args: &[&mut Dynamic], - ) -> String { - #[cfg(not(feature = "no_module"))] - let (ns, sep) = ( - namespace.to_string(), - if namespace.is_empty() { - "" +/// Generate the signature for a function call. +#[inline] +#[must_use] +pub fn gen_fn_call_signature(engine: &Engine, fn_name: &str, args: &[&mut Dynamic]) -> String { + format!( + "{fn_name} ({})", + args.iter() + .map(|a| if a.is::() { + "&str | ImmutableString | String" } else { - crate::tokenizer::Token::DoubleColon.literal_syntax() - }, - ); - #[cfg(feature = "no_module")] - let (ns, sep) = ("", ""); + engine.map_type_name(a.type_name()) + }) + .collect::>() + .join(", ") + ) +} - format!( - "{ns}{sep}{fn_name} ({})", - args.iter() - .map(|a| if a.is::() { - "&str | ImmutableString | String" - } else { - self.map_type_name(a.type_name()) - }) - .collect::>() - .join(", ") - ) - } +/// Generate the signature for a namespace-qualified function call. +/// +/// Not available under `no_module`. +#[cfg(not(feature = "no_module"))] +#[inline] +#[must_use] +pub fn gen_qualified_fn_call_signature( + engine: &Engine, + namespace: &crate::ast::Namespace, + fn_name: &str, + args: &[&mut Dynamic], +) -> String { + let (ns, sep) = ( + namespace.to_string(), + if namespace.is_empty() { + "" + } else { + crate::tokenizer::Token::DoubleColon.literal_syntax() + }, + ); + format!("{ns}{sep}{}", gen_fn_call_signature(engine, fn_name, args)) +} + +impl Engine { /// Resolve a normal (non-qualified) function call. /// /// Search order: @@ -405,11 +412,9 @@ impl Engine { 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) + func.get_plugin_fn().unwrap().call(context, args) } else { - func.get_native_fn().expect("native function")(context, args) + func.get_native_fn().unwrap()(context, args) }; // Restore the original reference @@ -541,16 +546,9 @@ impl Engine { } // Raise error - _ => Err(ERR::ErrorFunctionNotFound( - self.gen_fn_call_signature( - #[cfg(not(feature = "no_module"))] - &crate::ast::Namespace::NONE, - name, - args, - ), - pos, - ) - .into()), + _ => { + Err(ERR::ErrorFunctionNotFound(gen_fn_call_signature(self, name, args), pos).into()) + } } } @@ -1432,7 +1430,7 @@ impl Engine { Some(f) => unreachable!("unknown function type: {:?}", f), None => Err(ERR::ErrorFunctionNotFound( - self.gen_fn_call_signature(namespace, fn_name, &args), + gen_qualified_fn_call_signature(self, namespace, fn_name, &args), pos, ) .into()), diff --git a/src/func/mod.rs b/src/func/mod.rs index 50d444f1..d3887638 100644 --- a/src/func/mod.rs +++ b/src/func/mod.rs @@ -13,7 +13,9 @@ pub mod script; pub use args::FuncArgs; pub use builtin::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn}; -pub use call::FnCallArgs; +#[cfg(not(feature = "no_module"))] +pub use call::gen_qualified_fn_call_signature; +pub use call::{gen_fn_call_signature, FnCallArgs}; pub use callable_function::CallableFunction; #[cfg(not(feature = "no_function"))] pub use func::Func;