diff --git a/README.md b/README.md index 7edc5667..0339ae85 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,15 @@ to add scripting to any application. Rhai's current features set: -* Easy-to-use language similar to JS+Rust -* Easy integration with Rust [native functions](#working-with-functions) and [types](#custom-types-and-methods), +* Easy-to-use language similar to JS+Rust with dynamic typing but _no_ garbage collector +* Tight integration with native Rust [functions](#working-with-functions) and [types](#custom-types-and-methods), including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers) +* Freely pass Rust variables/constants into a script via an external [`Scope`] * Easily [call a script-defined function](#calling-rhai-functions-from-rust) from Rust -* Freely pass variables/constants into a script via an external [`Scope`] -* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop) -* Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app) -* Relatively little `unsafe` code (yes there are some for performance reasons) +* Low compile-time overhead (~0.6 sec debug/~3 sec release for `rhai_runner` sample app) +* Fairly efficient evaluation (1 million iterations in 0.75 sec on my 5 year old laptop) +* Relatively little `unsafe` code (yes there are some for performance reasons, and all `unsafe` code is limited to + one single source file, all with names starting with `"unsafe_"`) * Sand-boxed (the scripting [`Engine`] can be declared immutable which cannot mutate the containing environment unless explicitly allowed via `RefCell` etc.) * Rugged (protection against [stack-overflow](#maximum-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.) @@ -70,20 +71,21 @@ Optional features | Feature | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unchecked` | Exclude arithmetic checking (such as over-flows and division by zero), stack depth limit and operations count limit. Beware that a bad script may panic the entire system! | -| `no_function` | Disable script-defined functions if not needed. | -| `no_index` | Disable [arrays] and indexing features if not needed. | -| `no_object` | Disable support for custom types and objects. | -| `no_float` | Disable floating-point numbers and math if not needed. | +| `no_function` | Disable script-defined functions. | +| `no_index` | Disable [arrays] and indexing features. | +| `no_object` | Disable support for custom types and object maps. | +| `no_float` | Disable floating-point numbers and math. | | `no_optimize` | Disable the script optimizer. | | `no_module` | Disable modules. | | `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | -| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. | +| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and `AST`, are all `Send + Sync`. | By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed. -Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language. +Excluding unneeded functionalities can result in smaller, faster builds +as well as more control over what a script can (or cannot) do. [`unchecked`]: #optional-features [`no_index`]: #optional-features @@ -967,7 +969,7 @@ Indexers -------- Custom types can also expose an _indexer_ by registering an indexer function. -A custom with an indexer function defined can use the bracket '`[]`' notation to get a property value +A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value (but not update it - indexers are read-only). ```rust diff --git a/src/engine.rs b/src/engine.rs index 6423ca6b..2c9a3a82 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -72,19 +72,36 @@ enum Target<'a> { /// The target is a mutable reference to a `Dynamic` value somewhere. Ref(&'a mut Dynamic), /// The target is a temporary `Dynamic` value (i.e. the mutation can cause no side effects). - Value(Box), + Value(Dynamic), /// The target is a character inside a String. /// This is necessary because directly pointing to a char inside a String is impossible. - StringChar(Box<(&'a mut Dynamic, usize, Dynamic)>), + StringChar(&'a mut Dynamic, usize, Dynamic), } impl Target<'_> { - /// Get the value of the `Target` as a `Dynamic`. + /// Is the `Target` a reference pointing to other data? + pub fn is_ref(&self) -> bool { + match self { + Target::Ref(_) => true, + Target::Value(_) | Target::StringChar(_, _, _) => false, + } + } + + /// Get the value of the `Target` as a `Dynamic`, cloning a referenced value if necessary. pub fn clone_into_dynamic(self) -> Dynamic { match self { - Target::Ref(r) => r.clone(), - Target::Value(v) => *v, - Target::StringChar(s) => s.2, + Target::Ref(r) => r.clone(), // Referenced value is cloned + Target::Value(v) => v, // Owned value is simply taken + Target::StringChar(_, _, ch) => ch, // Character is taken + } + } + + /// Get a mutable reference from the `Target`. + pub fn as_mut(&mut self) -> &mut Dynamic { + match self { + Target::Ref(r) => *r, + Target::Value(ref mut r) => r, + Target::StringChar(_, _, ref mut r) => r, } } @@ -95,25 +112,23 @@ impl Target<'_> { Target::Value(_) => { return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS(pos))) } - Target::StringChar(x) => match x.0 { - Dynamic(Union::Str(s)) => { - // Replace the character at the specified index position - let new_ch = new_val - .as_char() - .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; + Target::StringChar(Dynamic(Union::Str(s)), index, _) => { + // Replace the character at the specified index position + let new_ch = new_val + .as_char() + .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; - let mut chars: StaticVec = s.chars().collect(); - let ch = *chars.get_ref(x.1); + let mut chars: StaticVec = s.chars().collect(); + let ch = *chars.get_ref(*index); - // See if changed - if so, update the String - if ch != new_ch { - *chars.get_mut(x.1) = new_ch; - s.clear(); - chars.iter().for_each(|&ch| s.push(ch)); - } + // See if changed - if so, update the String + if ch != new_ch { + *chars.get_mut(*index) = new_ch; + s.clear(); + chars.iter().for_each(|&ch| s.push(ch)); } - _ => unreachable!(), - }, + } + _ => unreachable!(), } Ok(()) @@ -127,7 +142,7 @@ impl<'a> From<&'a mut Dynamic> for Target<'a> { } impl> From for Target<'_> { fn from(value: T) -> Self { - Self::Value(Box::new(value.into())) + Self::Value(value.into()) } } @@ -913,7 +928,7 @@ impl Engine { fn eval_dot_index_chain_helper( &self, state: &mut State, - mut target: Target, + target: &mut Target, rhs: &Expr, idx_values: &mut StaticVec, is_index: bool, @@ -921,12 +936,10 @@ impl Engine { level: usize, mut new_val: Option, ) -> Result<(Dynamic, bool), Box> { + let is_ref = target.is_ref(); + // Get a reference to the mutation target Dynamic - let (obj, is_ref) = match target { - Target::Ref(r) => (r, true), - Target::Value(ref mut r) => (r.as_mut(), false), - Target::StringChar(ref mut x) => (&mut x.2, false), - }; + let obj = target.as_mut(); // Pop the last index value let mut idx_val = idx_values.pop(); @@ -937,20 +950,20 @@ impl Engine { Expr::Dot(x) | Expr::Index(x) => { let is_idx = matches!(rhs, Expr::Index(_)); let pos = x.0.position(); - let val = - self.get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, false)?; + let this_ptr = &mut self + .get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, false)?; self.eval_dot_index_chain_helper( - state, val, &x.1, idx_values, is_idx, x.2, level, new_val, + state, this_ptr, &x.1, idx_values, is_idx, x.2, level, new_val, ) } // xxx[rhs] = new_val _ if new_val.is_some() => { let pos = rhs.position(); - let mut val = - self.get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, true)?; + let this_ptr = &mut self + .get_indexed_mut(state, obj, is_ref, idx_val, pos, op_pos, true)?; - val.set_value(new_val.unwrap(), rhs.position())?; + this_ptr.set_value(new_val.unwrap(), rhs.position())?; Ok((Default::default(), true)) } // xxx[rhs] @@ -1019,7 +1032,7 @@ impl Engine { Expr::Index(x) | Expr::Dot(x) if obj.is::() => { let is_idx = matches!(rhs, Expr::Index(_)); - let val = if let Expr::Property(p) = &x.0 { + let mut val = if let Expr::Property(p) = &x.0 { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); self.get_indexed_mut(state, obj, is_ref, index, x.2, op_pos, false)? @@ -1032,7 +1045,7 @@ impl Engine { }; self.eval_dot_index_chain_helper( - state, val, &x.1, idx_values, is_idx, x.2, level, new_val, + state, &mut val, &x.1, idx_values, is_idx, x.2, level, new_val, ) } // xxx.idx_lhs[idx_expr] | xxx.dot_lhs.rhs @@ -1051,16 +1064,10 @@ impl Engine { ))); }; let val = &mut val; + let target = &mut val.into(); let (result, may_be_changed) = self.eval_dot_index_chain_helper( - state, - val.into(), - &x.1, - idx_values, - is_idx, - x.2, - level, - new_val, + state, target, &x.1, idx_values, is_idx, x.2, level, new_val, )?; // Feed the value back via a setter just in case it has been updated @@ -1125,7 +1132,7 @@ impl Engine { ScopeEntryType::Constant | ScopeEntryType::Normal => (), } - let this_ptr = target.into(); + let this_ptr = &mut target.into(); self.eval_dot_index_chain_helper( state, this_ptr, dot_rhs, idx_values, is_index, op_pos, level, new_val, ) @@ -1140,7 +1147,7 @@ impl Engine { // {expr}.??? or {expr}[???] expr => { let val = self.eval_expr(scope, state, expr, level)?; - let this_ptr = val.into(); + let this_ptr = &mut val.into(); self.eval_dot_index_chain_helper( state, this_ptr, dot_rhs, idx_values, is_index, op_pos, level, new_val, ) @@ -1258,7 +1265,7 @@ impl Engine { let ch = s.chars().nth(offset).ok_or_else(|| { Box::new(EvalAltResult::ErrorStringBounds(chars_len, index, idx_pos)) })?; - Ok(Target::StringChar(Box::new((val, offset, ch.into())))) + Ok(Target::StringChar(val, offset, ch.into())) } else { Err(Box::new(EvalAltResult::ErrorStringBounds( chars_len, index, idx_pos, diff --git a/src/unsafe.rs b/src/unsafe.rs index 65ae0b45..64ba8904 100644 --- a/src/unsafe.rs +++ b/src/unsafe.rs @@ -56,6 +56,8 @@ pub fn unsafe_cast_var_name<'s>(name: &str, state: &State) -> Cow<'s, str> { // this is safe because all local variables are cleared at the end of the block unsafe { mem::transmute::<_, &'s str>(name) }.into() } else { + // The variable is introduced at global (top) level and may persist after the script run. + // Therefore, clone the variable name. name.to_string().into() } } diff --git a/tests/get_set.rs b/tests/get_set.rs index 57712304..94c3064a 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -43,20 +43,20 @@ fn test_get_set() -> Result<(), Box> { engine.register_fn("new_ts", TestStruct::new); #[cfg(not(feature = "no_index"))] - engine.register_indexer(|value: &mut TestStruct, index: INT| value.array[index as usize]); + engine.register_indexer(|value: &mut TestStruct, index: String| value.array[index.len()]); assert_eq!(engine.eval::("let a = new_ts(); a.x = 500; a.x")?, 500); assert_eq!(engine.eval::("let a = new_ts(); a.x.add(); a.x")?, 42); assert_eq!(engine.eval::("let a = new_ts(); a.y.add(); a.y")?, 0); #[cfg(not(feature = "no_index"))] - assert_eq!(engine.eval::("let a = new_ts(); a[3]")?, 4); + assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); Ok(()) } #[test] -fn test_big_get_set() -> Result<(), Box> { +fn test_get_set_chain() -> Result<(), Box> { #[derive(Clone)] struct TestChild { x: INT,