diff --git a/README.md b/README.md index 8120cb6c..ad4a5037 100644 --- a/README.md +++ b/README.md @@ -395,14 +395,15 @@ are supported. ### Built-in operators -| Operator | Supported for type (see [standard types]) | -| ---------------------------- | ----------------------------------------------------------------------------- | -| `+`, `-`, `*`, `/`, `%`, `~` | `INT`, `FLOAT` (if not [`no_float`]) | -| `<<`, `>>`, `^` | `INT` | -| `&`, `\|` | `INT`, `bool` | -| `&&`, `\|\|` | `bool` | -| `==`, `!=` | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | -| `>`, `>=`, `<`, `<=` | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | +| Operators | Assignment operators | Supported for type (see [standard types]) | +| ------------------------ | ---------------------------- | ----------------------------------------------------------------------------- | +| `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `ImmutableString` | +| `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) | +| `<<`, `>>`, `^`, | `<<=`, `>>=`, `^=` | `INT` | +| `&`, `\|`, | `&=`, `|=` | `INT`, `bool` | +| `&&`, `\|\|` | | `bool` | +| `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` | +| `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` | ### Packages @@ -426,20 +427,20 @@ engine.load_package(package.get()); // load the package manually. 'g The follow packages are available: -| Package | Description | In `CorePackage` | In `StandardPackage` | -| ---------------------- | -------------------------------------------------------------------------- | :--------------: | :------------------: | -| `ArithmeticPackage` | Arithmetic operators (e.g. `+`, `-`, `*`, `/`) for different numeric types | Yes | Yes | -| `BasicIteratorPackage` | Numeric ranges (e.g. `range(1, 10)`) | Yes | Yes | -| `LogicPackage` | Logical and comparison operators (e.g. `==`, `>`) | Yes | Yes | -| `BasicStringPackage` | Basic string functions | Yes | Yes | -| `BasicTimePackage` | Basic time functions (e.g. [timestamps]) | Yes | Yes | -| `MoreStringPackage` | Additional string functions | No | Yes | -| `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | -| `BasicArrayPackage` | Basic [array] functions | No | Yes | -| `BasicMapPackage` | Basic [object map] functions | No | Yes | -| `EvalPackage` | Disable [`eval`] | No | No | -| `CorePackage` | Basic essentials | | | -| `StandardPackage` | Standard library | | | +| Package | Description | In `CorePackage` | In `StandardPackage` | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | :--------------: | :------------------: | +| `ArithmeticPackage` | Arithmetic operators (e.g. `+`, `-`, `*`, `/`) for numeric types that are not built in (e.g. `u16`) | Yes | Yes | +| `BasicIteratorPackage` | Numeric ranges (e.g. `range(1, 10)`) | Yes | Yes | +| `LogicPackage` | Logical and comparison operators (e.g. `==`, `>`) for numeric types that are not built in (e.g. `u16`) | Yes | Yes | +| `BasicStringPackage` | Basic string functions (e.g. `print`, `debug`, `len`) that are not built in | Yes | Yes | +| `BasicTimePackage` | Basic time functions (e.g. [timestamps]) | Yes | Yes | +| `MoreStringPackage` | Additional string functions, including converting common types to string | No | Yes | +| `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | +| `BasicArrayPackage` | Basic [array] functions (not available under `no_index`) | No | Yes | +| `BasicMapPackage` | Basic [object map] functions (not available under `no_object`) | No | Yes | +| `EvalPackage` | Disable [`eval`] | No | No | +| `CorePackage` | Basic essentials | Yes | Yes | +| `StandardPackage` | Standard library | No | Yes | Packages typically contain Rust functions that are callable within a Rhai script. All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers). diff --git a/RELEASES.md b/RELEASES.md index 56e04b6b..bc5a2125 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -34,9 +34,11 @@ Speed enhancements ------------------ * Common operators (e.g. `+`, `>`, `==`) now call into highly efficient built-in implementations for standard types - (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and some `String`) if not overridden by a registered function. + (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. This yields a 5-10% speed benefit depending on script operator usage. Scripts running tight loops will see significant speed-up. +* Common assignment operators (e.g. `+=`, `%=`) now call into highly efficient built-in implementations for + standard types (i.e. `INT`, `FLOAT`, `bool`, `char`, `()` and `ImmutableString`) if not overridden by a registered function. * Implementations of common operators for standard types are removed from the `ArithmeticPackage` and `LogicPackage` (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. * Operator-assignment statements (e.g. `+=`) are now handled directly and much faster. diff --git a/src/engine.rs b/src/engine.rs index f4f85b9d..276ce86c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{FnCallArgs, Shared}; +use crate::fn_native::{CallableFunction, FnCallArgs, Shared}; use crate::module::Module; use crate::optimize::OptimizationLevel; use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; @@ -438,14 +438,18 @@ fn default_print(s: &str) { /// Search for a variable within the scope fn search_scope<'a>( scope: &'a mut Scope, - name: &str, - modules: Option<(&ModuleRef, u64)>, - index: Option, - pos: Position, -) -> Result<(&'a mut Dynamic, ScopeEntryType), Box> { + state: &mut State, + expr: &'a Expr, +) -> Result<(&'a mut Dynamic, &'a str, ScopeEntryType, Position), Box> { + let ((name, pos), modules, hash_var, index) = match expr { + Expr::Variable(x) => x.as_ref(), + _ => unreachable!(), + }; + let index = if state.always_search { None } else { *index }; + #[cfg(not(feature = "no_module"))] { - if let Some((modules, hash_var)) = modules { + if let Some(modules) = modules.as_ref() { let module = if let Some(index) = modules.index() { scope .get_mut(scope.len() - index.get()) @@ -461,9 +465,11 @@ fn search_scope<'a>( }; return Ok(( - module.get_qualified_var_mut(name, hash_var, pos)?, + module.get_qualified_var_mut(name, *hash_var, *pos)?, + name, // Module variables are constant ScopeEntryType::Constant, + *pos, )); } } @@ -473,11 +479,12 @@ fn search_scope<'a>( } else { scope .get_index(name) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), pos)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? .0 }; - Ok(scope.get_mut(index)) + let (val, typ) = scope.get_mut(index); + Ok((val, name, typ, *pos)) } impl Engine { @@ -677,11 +684,8 @@ impl Engine { }); } - // See if it is built in. Only consider situations where: - // 1) It is not a method call, - // 2) the call explicitly specifies `native_only`, - // 3) there are two parameters. - if !is_ref && native_only && args.len() == 2 { + // See if it is built in. + if args.len() == 2 { match run_builtin_binary_op(fn_name, args[0], args[1])? { Some(v) => return Ok((v, false)), None => (), @@ -1147,20 +1151,17 @@ impl Engine { match dot_lhs { // id.??? or id[???] - Expr::Variable(x) => { - let ((name, pos), modules, hash_var, index) = x.as_ref(); - let index = if state.always_search { None } else { *index }; - let mod_and_hash = modules.as_ref().map(|m| (m.as_ref(), *hash_var)); - let (target, typ) = search_scope(scope, &name, mod_and_hash, index, *pos)?; - self.inc_operations(state, *pos)?; + Expr::Variable(_) => { + let (target, name, typ, pos) = search_scope(scope, state, dot_lhs)?; + self.inc_operations(state, pos)?; // Constants cannot be modified match typ { ScopeEntryType::Module => unreachable!(), ScopeEntryType::Constant if new_val.is_some() => { return Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( - name.clone(), - *pos, + name.to_string(), + pos, ))); } ScopeEntryType::Constant | ScopeEntryType::Normal => (), @@ -1404,11 +1405,8 @@ impl Engine { Expr::FloatConstant(x) => Ok(x.0.into()), Expr::StringConstant(x) => Ok(x.0.to_string().into()), Expr::CharConstant(x) => Ok(x.0.into()), - Expr::Variable(x) => { - let ((name, pos), modules, hash_var, index) = x.as_ref(); - let index = if state.always_search { None } else { *index }; - let mod_and_hash = modules.as_ref().map(|m| (m.as_ref(), *hash_var)); - let (val, _) = search_scope(scope, name, mod_and_hash, index, *pos)?; + Expr::Variable(_) => { + let (val, _, _, _) = search_scope(scope, state, expr)?; Ok(val.clone()) } Expr::Property(_) => unreachable!(), @@ -1416,89 +1414,106 @@ impl Engine { // Statement block Expr::Stmt(stmt) => self.eval_stmt(scope, state, lib, &stmt.0, level), - // lhs = rhs + // var op= rhs + Expr::Assignment(x) if matches!(x.0, Expr::Variable(_)) => { + let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); + let mut rhs_val = self.eval_expr(scope, state, lib, rhs_expr, level)?; + let (lhs_ptr, name, typ, pos) = search_scope(scope, state, lhs_expr)?; + self.inc_operations(state, pos)?; + + match typ { + // Assignment to constant variable + ScopeEntryType::Constant => Err(Box::new( + EvalAltResult::ErrorAssignmentToConstant(name.to_string(), pos), + )), + // Normal assignment + ScopeEntryType::Normal if op.is_empty() => { + *lhs_ptr = rhs_val; + Ok(Default::default()) + } + // Op-assignment - in order of precedence: + ScopeEntryType::Normal => { + // 1) Native registered overriding function + // 2) Built-in implementation + // 3) Map to `var = var op rhs` + + // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. + let arg_types = once(lhs_ptr.type_id()).chain(once(rhs_val.type_id())); + let hash_fn = calc_fn_hash(empty(), op, 2, arg_types); + + if let Some(CallableFunction::Method(func)) = self + .global_module + .get_fn(hash_fn) + .or_else(|| self.packages.get_fn(hash_fn)) + { + // Overriding exact implementation + func(&mut [lhs_ptr, &mut rhs_val])?; + } else if run_builtin_op_assignment(op, lhs_ptr, &rhs_val)?.is_none() { + // Not built in, map to `var = var op rhs` + let op = &op[..op.len() - 1]; // extract operator without = + let hash = calc_fn_hash(empty(), op, 2, empty()); + let args = &mut [&mut lhs_ptr.clone(), &mut rhs_val]; + + // Set variable value + *lhs_ptr = self + .exec_fn_call( + state, lib, op, true, hash, args, false, None, *op_pos, level, + ) + .map(|(v, _)| v)?; + } + Ok(Default::default()) + } + // A module cannot be assigned to + ScopeEntryType::Module => unreachable!(), + } + } + + // lhs op= rhs Expr::Assignment(x) => { - let lhs_expr = &x.0; - let op = x.1.as_ref(); - let op_pos = x.3; - let mut rhs_val = self.eval_expr(scope, state, lib, &x.2, level)?; + let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); + let mut rhs_val = self.eval_expr(scope, state, lib, rhs_expr, level)?; - // name op= rhs - if let Expr::Variable(x) = &x.0 { - let ((name, pos), modules, hash_var, index) = x.as_ref(); - let index = if state.always_search { None } else { *index }; - let mod_and_hash = modules.as_ref().map(|m| (m.as_ref(), *hash_var)); - let (lhs_ptr, typ) = search_scope(scope, name, mod_and_hash, index, *pos)?; - self.inc_operations(state, *pos)?; - - match typ { - ScopeEntryType::Constant => Err(Box::new( - EvalAltResult::ErrorAssignmentToConstant(name.clone(), *pos), - )), - ScopeEntryType::Normal if !op.is_empty() => { - // Complex op-assignment - if run_builtin_op_assignment(op, lhs_ptr, &rhs_val)?.is_none() { - // Not built in, do function call - let mut lhs_val = lhs_ptr.clone(); - let args = &mut [&mut lhs_val, &mut rhs_val]; - let hash = calc_fn_hash(empty(), op, 2, empty()); - *lhs_ptr = self - .exec_fn_call( - state, lib, op, true, hash, args, false, None, op_pos, - level, - ) - .map(|(v, _)| v)?; - } - Ok(Default::default()) - } - ScopeEntryType::Normal => { - *lhs_ptr = rhs_val; - Ok(Default::default()) - } - // End variable cannot be a module - ScopeEntryType::Module => unreachable!(), - } + let new_val = Some(if op.is_empty() { + // Normal assignment + rhs_val } else { - let new_val = Some(if op.is_empty() { - rhs_val - } else { - // Complex op-assignment - always do function call - let args = &mut [ - &mut self.eval_expr(scope, state, lib, lhs_expr, level)?, - &mut rhs_val, - ]; - let hash = calc_fn_hash(empty(), op, 2, empty()); - self.exec_fn_call( - state, lib, op, true, hash, args, false, None, op_pos, level, - ) - .map(|(v, _)| v)? - }); + // Op-assignment - always map to `lhs = lhs op rhs` + let op = &op[..op.len() - 1]; // extract operator without = + let hash = calc_fn_hash(empty(), op, 2, empty()); + let args = &mut [ + &mut self.eval_expr(scope, state, lib, lhs_expr, level)?, + &mut rhs_val, + ]; + self.exec_fn_call( + state, lib, op, true, hash, args, false, None, *op_pos, level, + ) + .map(|(v, _)| v)? + }); - match &x.0 { - // name op= rhs - Expr::Variable(_) => unreachable!(), - // idx_lhs[idx_expr] op= rhs - #[cfg(not(feature = "no_index"))] - Expr::Index(x) => self.eval_dot_index_chain( - scope, state, lib, &x.0, &x.1, true, x.2, level, new_val, - ), - // dot_lhs.dot_rhs op= rhs - #[cfg(not(feature = "no_object"))] - Expr::Dot(x) => self.eval_dot_index_chain( - scope, state, lib, &x.0, &x.1, false, op_pos, level, new_val, - ), - // Error assignment to constant - expr if expr.is_constant() => { - Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( - expr.get_constant_str(), - expr.position(), - ))) - } - // Syntax error - expr => Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS( + match lhs_expr { + // name op= rhs + Expr::Variable(_) => unreachable!(), + // idx_lhs[idx_expr] op= rhs + #[cfg(not(feature = "no_index"))] + Expr::Index(x) => self.eval_dot_index_chain( + scope, state, lib, &x.0, &x.1, true, x.2, level, new_val, + ), + // dot_lhs.dot_rhs op= rhs + #[cfg(not(feature = "no_object"))] + Expr::Dot(x) => self.eval_dot_index_chain( + scope, state, lib, &x.0, &x.1, false, *op_pos, level, new_val, + ), + // Error assignment to constant + expr if expr.is_constant() => { + Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( + expr.get_constant_str(), expr.position(), - ))), + ))) } + // Syntax error + expr => Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS( + expr.position(), + ))), } } @@ -2132,34 +2147,34 @@ fn run_builtin_op_assignment( #[cfg(not(feature = "unchecked"))] match op { - "+" => return Ok(Some(*x = add(*x, y)?)), - "-" => return Ok(Some(*x = sub(*x, y)?)), - "*" => return Ok(Some(*x = mul(*x, y)?)), - "/" => return Ok(Some(*x = div(*x, y)?)), - "%" => return Ok(Some(*x = modulo(*x, y)?)), - "~" => return Ok(Some(*x = pow_i_i(*x, y)?)), - ">>" => return Ok(Some(*x = shr(*x, y)?)), - "<<" => return Ok(Some(*x = shl(*x, y)?)), + "+=" => return Ok(Some(*x = add(*x, y)?)), + "-=" => return Ok(Some(*x = sub(*x, y)?)), + "*=" => return Ok(Some(*x = mul(*x, y)?)), + "/=" => return Ok(Some(*x = div(*x, y)?)), + "%=" => return Ok(Some(*x = modulo(*x, y)?)), + "~=" => return Ok(Some(*x = pow_i_i(*x, y)?)), + ">>=" => return Ok(Some(*x = shr(*x, y)?)), + "<<=" => return Ok(Some(*x = shl(*x, y)?)), _ => (), } #[cfg(feature = "unchecked")] match op { - "+" => return Ok(Some(*x += y)), - "-" => return Ok(Some(*x -= y)), - "*" => return Ok(Some(*x *= y)), - "/" => return Ok(Some(*x /= y)), - "%" => return Ok(Some(*x %= y)), - "~" => return Ok(Some(*x = pow_i_i_u(*x, y)?)), - ">>" => return Ok(Some(*x = shr_u(*x, y)?)), - "<<" => return Ok(Some(*x = shl_u(*x, y)?)), + "+=" => return Ok(Some(*x += y)), + "-=" => return Ok(Some(*x -= y)), + "*=" => return Ok(Some(*x *= y)), + "/=" => return Ok(Some(*x /= y)), + "%=" => return Ok(Some(*x %= y)), + "~=" => return Ok(Some(*x = pow_i_i_u(*x, y)?)), + ">>=" => return Ok(Some(*x = shr_u(*x, y)?)), + "<<=" => return Ok(Some(*x = shl_u(*x, y)?)), _ => (), } match op { - "&" => return Ok(Some(*x &= y)), - "|" => return Ok(Some(*x |= y)), - "^" => return Ok(Some(*x ^= y)), + "&=" => return Ok(Some(*x &= y)), + "|=" => return Ok(Some(*x |= y)), + "^=" => return Ok(Some(*x ^= y)), _ => (), } } else if args_type == TypeId::of::() { @@ -2167,8 +2182,8 @@ fn run_builtin_op_assignment( let y = y.downcast_ref::().unwrap().clone(); match op { - "&" => return Ok(Some(*x = *x && y)), - "|" => return Ok(Some(*x = *x || y)), + "&=" => return Ok(Some(*x = *x && y)), + "|=" => return Ok(Some(*x = *x || y)), _ => (), } } @@ -2180,12 +2195,12 @@ fn run_builtin_op_assignment( let y = y.downcast_ref::().unwrap().clone(); match op { - "+" => return Ok(Some(*x += y)), - "-" => return Ok(Some(*x -= y)), - "*" => return Ok(Some(*x *= y)), - "/" => return Ok(Some(*x /= y)), - "%" => return Ok(Some(*x %= y)), - "~" => return Ok(Some(*x = pow_f_f(*x, y)?)), + "+=" => return Ok(Some(*x += y)), + "-=" => return Ok(Some(*x -= y)), + "*=" => return Ok(Some(*x *= y)), + "/=" => return Ok(Some(*x /= y)), + "%=" => return Ok(Some(*x %= y)), + "~=" => return Ok(Some(*x = pow_f_f(*x, y)?)), _ => (), } } diff --git a/src/parser.rs b/src/parser.rs index 02e83c97..24e6f76e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1444,17 +1444,18 @@ fn parse_op_assignment_stmt<'a>( let op = match token { Token::Equals => "".into(), - Token::PlusAssign => Token::Plus.syntax(), - Token::MinusAssign => Token::Minus.syntax(), - Token::MultiplyAssign => Token::Multiply.syntax(), - Token::DivideAssign => Token::Divide.syntax(), - Token::LeftShiftAssign => Token::LeftShift.syntax(), - Token::RightShiftAssign => Token::RightShift.syntax(), - Token::ModuloAssign => Token::Modulo.syntax(), - Token::PowerOfAssign => Token::PowerOf.syntax(), - Token::AndAssign => Token::Ampersand.syntax(), - Token::OrAssign => Token::Pipe.syntax(), - Token::XOrAssign => Token::XOr.syntax(), + + Token::PlusAssign + | Token::MinusAssign + | Token::MultiplyAssign + | Token::DivideAssign + | Token::LeftShiftAssign + | Token::RightShiftAssign + | Token::ModuloAssign + | Token::PowerOfAssign + | Token::AndAssign + | Token::OrAssign + | Token::XOrAssign => token.syntax(), _ => return Ok(lhs), };