diff --git a/CHANGELOG.md b/CHANGELOG.md index a982c131..3ea21d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Enhancements * `Engine::consume_XXX` methods are renamed to `Engine::run_XXX` to make meanings clearer. The `consume_XXX` API is deprecated. * `Engine::register_type_XXX` are now available even under `no_object`. * Added `Engine::on_parse_token` to allow remapping certain tokens during parsing. +* Added `Engine::const_empty_string` to merge empty strings into a single instance. ### Custom Syntax @@ -42,6 +43,7 @@ Enhancements * `SmartString` now uses `LazyCompact` instead of `Compact` to minimize allocations. * Added `pop` for strings. +* Added `ImmutableString::ptr_eq` to test if two strings point to the same allocation. ### `Scope` API @@ -57,9 +59,22 @@ Enhancements * `StaticVec` is changed to keep three items inline instead of four. +Version 1.0.7 +============= + + Version 1.0.6 ============= +Bug fixes +--------- + +* Eliminate unnecessary property write-back when accessed via a getter since property getters are assumed to be _pure_. +* Writing to a property of an indexed valued obtained via an indexer now works properly by writing back the changed value via an index setter. + +Enhancements +------------ + * `MultiInputsStream`, `ParseState`, `TokenIterator`, `IdentifierBuilder` and `AccessMode` are exported under the `internals` feature. diff --git a/README.md b/README.md index f3cef610..e11cf72f 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Below is the standard _Fibonacci_ example for scripting languages: const TARGET = 28; const REPEAT = 5; +const ANSWER = 317_811; fn fib(n) { if n < 2 { @@ -107,8 +108,8 @@ print(`Finished. Run time = ${now.elapsed} seconds.`); print(`Fibonacci number #${TARGET} = ${result}`); -if result != 317_811 { - print("The answer is WRONG! Should be 317,811!"); +if result != ANSWER { + print(`The answer is WRONG! Should be ${ANSWER}!`); } ``` diff --git a/codegen/ui_tests/rhai_mod_unknown_type.stderr b/codegen/ui_tests/rhai_mod_unknown_type.stderr index 9ea2728f..5853808f 100644 --- a/codegen/ui_tests/rhai_mod_unknown_type.stderr +++ b/codegen/ui_tests/rhai_mod_unknown_type.stderr @@ -13,9 +13,9 @@ help: a struct with a similar name exists | ~~~~~ help: consider importing one of these items | -11 | use core::fmt::Pointer; - | 11 | use std::fmt::Pointer; | 11 | use syn::__private::fmt::Pointer; | +11 | use core::fmt::Pointer; + | diff --git a/scripts/fibonacci.rhai b/scripts/fibonacci.rhai index dac77d22..8451c477 100644 --- a/scripts/fibonacci.rhai +++ b/scripts/fibonacci.rhai @@ -3,6 +3,7 @@ const TARGET = 28; const REPEAT = 5; +const ANSWER = 317_811; fn fib(n) { if n < 2 { @@ -26,6 +27,6 @@ print(`Finished. Run time = ${now.elapsed} seconds.`); print(`Fibonacci number #${TARGET} = ${result}`); -if result != 317_811 { - print("The answer is WRONG! Should be 317,811!"); +if result != ANSWER { + print(`The answer is WRONG! Should be ${ANSWER}!`); } diff --git a/src/custom_syntax.rs b/src/custom_syntax.rs index eaee3c27..7f98ae18 100644 --- a/src/custom_syntax.rs +++ b/src/custom_syntax.rs @@ -316,6 +316,24 @@ impl Engine { /// /// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`]. /// Otherwise, they won't be recognized. + /// + /// # Implementation Function Signature + /// + /// The implementation function has the following signature: + /// + /// > `Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result, ParseError>` + /// + /// where: + /// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`; + /// `$ident$` and other literal markers are replaced by the actual text + /// * `look_ahead`: a string slice containing the next symbol that is about to be read + /// + /// ## Return value + /// + /// * `Ok(None)`: parsing complete and there are no more symbols to match. + /// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`. + /// * `Err(ParseError)`: error that is reflected back to the [`Engine`], normally `ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE)` to indicate a syntax error, but it can be any [`ParseError`]. + /// pub fn register_custom_syntax_raw( &mut self, key: impl Into, diff --git a/src/dynamic.rs b/src/dynamic.rs index 075ddab9..ae2096ab 100644 --- a/src/dynamic.rs +++ b/src/dynamic.rs @@ -1877,7 +1877,7 @@ impl Dynamic { _ => Err(self.type_name()), } } - /// Cast the [`Dynamic`] as an [`ImmutableString`] and return it. + /// Cast the [`Dynamic`] as an [`ImmutableString`] and return it as a string slice. /// Returns the name of the actual type if the cast fails. /// /// # Panics @@ -1888,7 +1888,7 @@ impl Dynamic { match self.0 { Union::Str(ref s, _, _) => Ok(s), #[cfg(not(feature = "no_closure"))] - Union::Shared(_, _, _) => panic!("as_str() cannot be called on shared values"), + Union::Shared(_, _, _) => panic!("as_str_ref() cannot be called on shared values"), _ => Err(self.type_name()), } } diff --git a/src/engine.rs b/src/engine.rs index 51391745..b0aa1400 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,7 @@ use crate::ast::{Expr, FnCallExpr, Ident, OpAssignment, Stmt, AST_OPTION_FLAGS::*}; use crate::custom_syntax::CustomSyntax; use crate::dynamic::{map_std_type_name, AccessMode, Union, Variant}; -use crate::fn_hash::get_hasher; +use crate::fn_hash::{calc_fn_hash, get_hasher}; use crate::fn_native::{ CallableFunction, IteratorFn, OnDebugCallback, OnParseTokenCallback, OnPrintCallback, OnVarCallback, @@ -336,6 +336,20 @@ impl ChainArgument { _ => None, } } + /// Return the [position][Position]. + #[inline(always)] + #[must_use] + #[allow(dead_code)] + pub const fn position(&self) -> Position { + match self { + #[cfg(not(feature = "no_object"))] + ChainArgument::Property(pos) => *pos, + #[cfg(not(feature = "no_object"))] + ChainArgument::MethodCallArgs(_, pos) => *pos, + #[cfg(not(feature = "no_index"))] + ChainArgument::IndexValue(_, pos) => *pos, + } + } } #[cfg(not(feature = "no_object"))] @@ -790,6 +804,31 @@ impl Default for Limits { } } +/// A type containing useful constants for the [`Engine`]. +#[derive(Debug)] +pub struct GlobalConstants { + /// An empty [`ImmutableString`] for cloning purposes. + pub(crate) empty_string: ImmutableString, + /// Function call hash to FN_IDX_GET + #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] + pub(crate) fn_hash_idx_get: u64, + /// Function call hash to FN_IDX_SET + #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] + pub(crate) fn_hash_idx_set: u64, +} + +impl Default for GlobalConstants { + fn default() -> Self { + Self { + empty_string: Default::default(), + #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] + fn_hash_idx_get: calc_fn_hash(FN_IDX_GET, 2), + #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] + fn_hash_idx_set: calc_fn_hash(FN_IDX_SET, 3), + } + } +} + /// Context of a script evaluation process. #[derive(Debug)] pub struct EvalContext<'a, 'x, 'px, 'm, 's, 'b, 't, 'pt> { @@ -911,8 +950,8 @@ pub struct Engine { /// A map mapping type names to pretty-print names. pub(crate) type_names: BTreeMap>, - /// An empty [`ImmutableString`] for cloning purposes. - pub(crate) empty_string: ImmutableString, + /// Useful constants + pub(crate) constants: GlobalConstants, /// A set of symbols to disable. pub(crate) disabled_symbols: BTreeSet, @@ -1042,7 +1081,7 @@ impl Engine { module_resolver: None, type_names: Default::default(), - empty_string: Default::default(), + constants: Default::default(), disabled_symbols: Default::default(), custom_keywords: Default::default(), custom_syntax: Default::default(), @@ -1070,6 +1109,16 @@ impl Engine { engine } + /// Get an empty [`ImmutableString`]. + /// + /// [`Engine`] keeps a single instance of an empty [`ImmutableString`] and uses this to create + /// shared instances for subsequent uses. This minimizes unnecessary allocations for empty strings. + #[inline(always)] + #[must_use] + pub fn const_empty_string(&self) -> ImmutableString { + self.constants.empty_string.clone() + } + /// Search for a module within an imports stack. #[inline] #[must_use] @@ -1244,6 +1293,7 @@ impl Engine { #[cfg(not(feature = "no_index"))] ChainType::Indexing => { let pos = rhs.position(); + let root_pos = idx_val.position(); let idx_val = idx_val .into_index_value() .expect("`chain_type` is `ChainType::Index`"); @@ -1253,17 +1303,50 @@ impl Engine { Expr::Dot(x, term, x_pos) | Expr::Index(x, term, x_pos) if !_terminate_chaining => { + let mut idx_val_for_setter = idx_val.clone(); let idx_pos = x.lhs.position(); - let obj_ptr = &mut self.get_indexed_mut( - mods, state, lib, target, idx_val, idx_pos, false, true, level, - )?; - let rhs_chain = match_chaining_type(rhs); - self.eval_dot_index_chain_helper( - mods, state, lib, this_ptr, obj_ptr, root, &x.rhs, *term, idx_values, - rhs_chain, level, new_val, - ) - .map_err(|err| err.fill_position(*x_pos)) + + let (try_setter, result) = { + let mut obj = self.get_indexed_mut( + mods, state, lib, target, idx_val, idx_pos, false, true, level, + )?; + let is_obj_temp_val = obj.is_temp_value(); + let obj_ptr = &mut obj; + + match self.eval_dot_index_chain_helper( + mods, state, lib, this_ptr, obj_ptr, root, &x.rhs, *term, + idx_values, rhs_chain, level, new_val, + ) { + Ok((result, true)) if is_obj_temp_val => { + (Some(obj.take_or_clone()), (result, true)) + } + Ok(result) => (None, result), + Err(err) => return Err(err.fill_position(*x_pos)), + } + }; + + if let Some(mut new_val) = try_setter { + // Try to call index setter if value is changed + let hash_set = + FnCallHashes::from_native(self.constants.fn_hash_idx_set); + let args = &mut [target, &mut idx_val_for_setter, &mut new_val]; + + if let Err(err) = self.exec_fn_call( + mods, state, lib, FN_IDX_SET, hash_set, args, is_ref_mut, true, + root_pos, None, level, + ) { + // Just ignore if there is no index setter + if !matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) { + return Err(err); + } + } + } + + self.check_data_size(target.as_ref()) + .map_err(|err| err.fill_position(root.1))?; + + Ok(result) } // xxx[rhs] op= new_val _ if new_val.is_some() => { @@ -1294,13 +1377,12 @@ impl Engine { if let Some(mut new_val) = try_setter { // Try to call index setter let hash_set = - FnCallHashes::from_native(crate::calc_fn_hash(FN_IDX_SET, 3)); + FnCallHashes::from_native(self.constants.fn_hash_idx_set); let args = &mut [target, &mut idx_val_for_setter, &mut new_val]; - let pos = Position::NONE; self.exec_fn_call( mods, state, lib, FN_IDX_SET, hash_set, args, is_ref_mut, true, - pos, None, level, + root_pos, None, level, )?; } @@ -1427,7 +1509,7 @@ impl Engine { EvalAltResult::ErrorDotExpr(_, _) => { let args = &mut [target, &mut name.into(), &mut new_val]; let hash_set = - FnCallHashes::from_native(crate::calc_fn_hash(FN_IDX_SET, 3)); + FnCallHashes::from_native(self.constants.fn_hash_idx_set); let pos = Position::NONE; self.exec_fn_call( @@ -1471,6 +1553,7 @@ impl Engine { } _ => Err(err), }, + // Assume getters are always pure |(v, _)| Ok((v, false)), ) } @@ -1524,7 +1607,9 @@ impl Engine { let hash_set = FnCallHashes::from_native(*hash_set); let mut arg_values = [target.as_mut(), &mut Default::default()]; let args = &mut arg_values[..1]; - let (mut val, updated) = self + + // Assume getters are always pure + let (mut val, _) = self .exec_fn_call( mods, state, lib, getter, hash_get, args, is_ref_mut, true, *pos, None, level, @@ -1568,7 +1653,7 @@ impl Engine { .map_err(|err| err.fill_position(*x_pos))?; // Feed the value back via a setter just in case it has been updated - if updated || may_be_changed { + if may_be_changed { // Re-use args because the first &mut parameter will not be consumed let mut arg_values = [target.as_mut(), val]; let args = &mut arg_values; @@ -1583,7 +1668,7 @@ impl Engine { let args = &mut [target.as_mut(), &mut name.into(), val]; let hash_set = FnCallHashes::from_native( - crate::calc_fn_hash(FN_IDX_SET, 3), + self.constants.fn_hash_idx_set, ); self.exec_fn_call( mods, state, lib, FN_IDX_SET, hash_set, args, @@ -1987,7 +2072,7 @@ impl Engine { _ if use_indexers => { let args = &mut [target, &mut idx]; - let hash_get = FnCallHashes::from_native(crate::calc_fn_hash(FN_IDX_GET, 2)); + let hash_get = FnCallHashes::from_native(self.constants.fn_hash_idx_get); let idx_pos = Position::NONE; self.exec_fn_call( @@ -2059,7 +2144,7 @@ impl Engine { // `... ${...} ...` Expr::InterpolatedString(x, pos) => { let mut pos = *pos; - let mut result: Dynamic = self.empty_string.clone().into(); + let mut result: Dynamic = self.const_empty_string().into(); for expr in x.iter() { let item = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; @@ -2330,7 +2415,7 @@ impl Engine { let args = &mut [lhs_ptr_inner, &mut new_val]; match self.call_native_fn(mods, state, lib, op, hash, args, true, true, op_pos) { - Err(err) if matches!(err.as_ref(), EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with(op)) => + Err(err) if matches!(*err, EvalAltResult::ErrorFunctionNotFound(ref f, _) if f.starts_with(op)) => { // Expand to `var = var op rhs` let op = &op[..op.len() - 1]; // extract operator without = @@ -3006,17 +3091,23 @@ impl Engine { } /// Check a result to ensure that the data size is within allowable limit. - #[cfg(feature = "unchecked")] - #[inline(always)] - fn check_return_value(&self, result: RhaiResult) -> RhaiResult { - result - } + fn check_return_value(&self, mut result: RhaiResult) -> RhaiResult { + if let Ok(ref mut r) = result { + // Concentrate all empty strings into one instance to save memory + if let Dynamic(crate::dynamic::Union::Str(s, _, _)) = r { + if s.is_empty() { + if !s.ptr_eq(&self.constants.empty_string) { + *s = self.const_empty_string(); + } + return result; + } + } - /// Check a result to ensure that the data size is within allowable limit. - #[cfg(not(feature = "unchecked"))] - #[inline(always)] - fn check_return_value(&self, result: RhaiResult) -> RhaiResult { - result.and_then(|r| self.check_data_size(&r).map(|_| r)) + #[cfg(not(feature = "unchecked"))] + self.check_data_size(&r)?; + } + + result } #[cfg(feature = "unchecked")] diff --git a/src/engine_api.rs b/src/engine_api.rs index ece66787..8ba01d01 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -1368,15 +1368,17 @@ impl Engine { }; let (stream, tokenizer_control) = engine.lex_raw( &scripts, - Some(if has_null { - &|token| match token { - // If `null` is present, make sure `null` is treated as a variable - Token::Reserved(s) if s == "null" => Token::Identifier(s), - _ => token, - } + if has_null { + Some(&|token, _, _| { + match token { + // If `null` is present, make sure `null` is treated as a variable + Token::Reserved(s) if s == "null" => Token::Identifier(s), + _ => token, + } + }) } else { - &|t| t - }), + None + }, ); let mut state = ParseState::new(engine, tokenizer_control); let ast = engine.parse_global_expr( @@ -2078,12 +2080,28 @@ impl Engine { } /// Provide a callback that will be invoked before each variable access. /// - /// # Return Value of Callback + /// # Callback Function Signature /// - /// Return `Ok(None)` to continue with normal variable access. - /// Return `Ok(Some(Dynamic))` as the variable's value. + /// The callback function signature takes the following form: /// - /// # Errors in Callback + /// > `Fn(name: &str, index: usize, context: &EvalContext)` + /// > ` -> Result, Box> + 'static` + /// + /// where: + /// * `index`: an offset from the bottom of the current [`Scope`] that the variable is supposed + /// to reside. Offsets start from 1, with 1 meaning the last variable in the current + /// [`Scope`]. Essentially the correct variable is at position `scope.len() - index`. + /// If `index` is zero, then there is no pre-calculated offset position and a search through the + /// current [`Scope`] must be performed. + /// + /// * `context`: the current [evaluation context][`EvalContext`]. + /// + /// ## Return value + /// + /// * `Ok(None)`: continue with normal variable access. + /// * `Ok(Some(Dynamic))`: the variable's value. + /// + /// ## Raising errors /// /// Return `Err(...)` if there is an error. /// @@ -2121,6 +2139,22 @@ impl Engine { /// _(internals)_ Provide a callback that will be invoked during parsing to remap certain tokens. /// Exported under the `internals` feature only. /// + /// # Callback Function Signature + /// + /// The callback function signature takes the following form: + /// + /// > `Fn(token: Token, pos: Position, state: &TokenizeState) -> Token` + /// + /// where: + /// * [`token`][crate::token::Token]: current token parsed + /// * [`pos`][`Position`]: location of the token + /// * [`state`][crate::token::TokenizeState]: current state of the tokenizer + /// + /// ## Raising errors + /// + /// It is possible to raise a parsing error by returning + /// [`Token::LexError`][crate::token::Token::LexError] as the mapped token. + /// /// # Example /// /// ``` @@ -2130,7 +2164,7 @@ impl Engine { /// let mut engine = Engine::new(); /// /// // Register a token mapper. - /// engine.on_parse_token(|token| { + /// engine.on_parse_token(|token, _, _| { /// match token { /// // Convert all integer literals to strings /// Token::IntegerConstant(n) => Token::StringConstant(n.to_string()), @@ -2153,8 +2187,12 @@ impl Engine { #[inline(always)] pub fn on_parse_token( &mut self, - callback: impl Fn(crate::token::Token) -> crate::token::Token + SendSync + 'static, + callback: impl Fn(crate::token::Token, Position, &crate::token::TokenizeState) -> crate::token::Token + + SendSync + + 'static, ) -> &mut Self { + use std::string::ParseError; + self.token_mapper = Some(Box::new(callback)); self } @@ -2162,6 +2200,17 @@ impl Engine { /// /// Not available under `unchecked`. /// + /// # Callback Function Signature + /// + /// The callback function signature takes the following form: + /// + /// > `Fn(counter: u64) -> Option` + /// + /// ## Return value + /// + /// * `None`: continue running the script. + /// * `Some(Dynamic)`: terminate the script with the specified exception value. + /// /// # Example /// /// ``` @@ -2234,6 +2283,17 @@ impl Engine { } /// Override default action of `debug` (print to stdout using [`println!`]) /// + /// # Callback Function Signature + /// + /// The callback function signature passed takes the following form: + /// + /// > `Fn(text: &str, source: Option<&str>, pos: Position)` + /// + /// where: + /// * `text`: the text to display + /// * `source`: current source, if any + /// * [`pos`][`Position`]: location of the `debug` call + /// /// # Example /// /// ``` diff --git a/src/fn_native.rs b/src/fn_native.rs index 60f46274..9fa9d008 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -4,7 +4,7 @@ use crate::ast::{FnAccess, FnCallHashes}; use crate::engine::Imports; use crate::fn_call::FnCallArgs; use crate::plugin::PluginFunction; -use crate::token::Token; +use crate::token::{Token, TokenizeState}; use crate::{ calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, Module, Position, RhaiResult, }; @@ -310,10 +310,11 @@ pub type OnDebugCallback = Box, Position) + Send + Syn /// A standard callback function for mapping tokens during parsing. #[cfg(not(feature = "sync"))] -pub type OnParseTokenCallback = dyn Fn(Token) -> Token; +pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token; /// A standard callback function for mapping tokens during parsing. #[cfg(feature = "sync")] -pub type OnParseTokenCallback = dyn Fn(Token) -> Token + Send + Sync + 'static; +pub type OnParseTokenCallback = + dyn Fn(Token, Position, &TokenizeState) -> Token + Send + Sync + 'static; /// A standard callback function for variable access. #[cfg(not(feature = "sync"))] diff --git a/src/fn_register.rs b/src/fn_register.rs index c20263ff..776bace3 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -44,9 +44,7 @@ pub fn by_value(data: &mut Dynamic) -> T { if TypeId::of::() == TypeId::of::<&str>() { // If T is `&str`, data must be `ImmutableString`, so map directly to it data.flatten_in_place(); - let ref_str = data - .as_str_ref() - .expect("argument passed by value is not shared"); + let ref_str = data.as_str_ref().expect("argument type is &str"); let ref_t = unsafe { mem::transmute::<_, &T>(&ref_str) }; ref_t.clone() } else if TypeId::of::() == TypeId::of::() { diff --git a/src/immutable_string.rs b/src/immutable_string.rs index 54e9bf88..8c146897 100644 --- a/src/immutable_string.rs +++ b/src/immutable_string.rs @@ -538,4 +538,27 @@ impl ImmutableString { pub(crate) fn make_mut(&mut self) -> &mut SmartString { shared_make_mut(&mut self.0) } + /// Returns `true` if the two [`ImmutableString`]'s point to the same allocation. + /// + /// # Example + /// + /// ``` + /// use rhai::ImmutableString; + /// + /// let s1: ImmutableString = "hello".into(); + /// let s2 = s1.clone(); + /// let s3: ImmutableString = "hello".into(); + /// + /// assert_eq!(s1, s2); + /// assert_eq!(s1, s3); + /// assert_eq!(s2, s3); + /// + /// assert!(s1.ptr_eq(&s2)); + /// assert!(!s1.ptr_eq(&s3)); + /// assert!(!s2.ptr_eq(&s3)); + /// ``` + #[inline(always)] + pub fn ptr_eq(&self, other: &Self) -> bool { + Shared::ptr_eq(&self.0, &other.0) + } } diff --git a/src/optimize.rs b/src/optimize.rs index 616f0db5..22878f5b 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -822,7 +822,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { // `` Expr::InterpolatedString(x, pos) if x.is_empty() => { state.set_dirty(); - *expr = Expr::StringConstant(state.engine.empty_string.clone(), *pos); + *expr = Expr::StringConstant(state.engine.const_empty_string(), *pos); } // `...` Expr::InterpolatedString(x, _) if x.len() == 1 && matches!(x[0], Expr::StringConstant(_, _)) => { diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 58a94777..fb8d29e7 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -126,7 +126,7 @@ mod string_functions { len: INT, ) -> ImmutableString { if string.is_empty() || len <= 0 { - return ctx.engine().empty_string.clone(); + return ctx.engine().const_empty_string(); } let mut chars = StaticVec::::with_capacity(len as usize); @@ -305,13 +305,13 @@ mod string_functions { len: INT, ) -> ImmutableString { if string.is_empty() { - return ctx.engine().empty_string.clone(); + return ctx.engine().const_empty_string(); } let mut chars = StaticVec::with_capacity(string.len()); let offset = if string.is_empty() || len <= 0 { - return ctx.engine().empty_string.clone(); + return ctx.engine().const_empty_string(); } else if start < 0 { if let Some(n) = start.checked_abs() { chars.extend(string.chars()); @@ -324,7 +324,7 @@ mod string_functions { 0 } } else if start as usize >= string.chars().count() { - return ctx.engine().empty_string.clone(); + return ctx.engine().const_empty_string(); } else { start as usize }; @@ -354,7 +354,7 @@ mod string_functions { start: INT, ) -> ImmutableString { if string.is_empty() { - ctx.engine().empty_string.clone() + ctx.engine().const_empty_string() } else { let len = string.len() as INT; sub_string(ctx, string, start, len) @@ -571,14 +571,14 @@ mod string_functions { if let Some(n) = start.checked_abs() { let num_chars = string.chars().count(); if n as usize > num_chars { - vec![ctx.engine().empty_string.clone().into(), string.into()] + vec![ctx.engine().const_empty_string().into(), string.into()] } else { let prefix: String = string.chars().take(num_chars - n as usize).collect(); let prefix_len = prefix.len(); vec![prefix.into(), string[prefix_len..].into()] } } else { - vec![ctx.engine().empty_string.clone().into(), string.into()] + vec![ctx.engine().const_empty_string().into(), string.into()] } } else { let prefix: String = string.chars().take(start as usize).collect(); diff --git a/src/token.rs b/src/token.rs index 9baaf22a..1ffef163 100644 --- a/src/token.rs +++ b/src/token.rs @@ -29,6 +29,10 @@ use rust_decimal::Decimal; use crate::engine::KEYWORD_IS_DEF_FN; /// _(internals)_ A type containing commands to control the tokenizer. +/// +/// # Volatile Data Structure +/// +/// This type is volatile and may change. #[derive(Debug, Clone, Eq, PartialEq, Hash, Copy, Default)] pub struct TokenizerControlBlock { /// Is the current tokenizer position within an interpolated text string? @@ -992,7 +996,7 @@ pub struct TokenizeState { /// Maximum length of a string. pub max_string_size: Option, /// Can the next token be a unary operator? - pub non_unary: bool, + pub next_token_cannot_be_unary: bool, /// Is the tokenizer currently inside a block comment? pub comment_level: usize, /// Include comments? @@ -1327,7 +1331,7 @@ pub fn get_next_token( // Save the last token's state if let Some((ref token, _)) = result { - state.non_unary = !token.is_next_unary(); + state.next_token_cannot_be_unary = !token.is_next_unary(); } result @@ -1678,10 +1682,12 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::Reserved("++".into()), start_pos)); } - ('+', _) if !state.non_unary => return Some((Token::UnaryPlus, start_pos)), + ('+', _) if !state.next_token_cannot_be_unary => { + return Some((Token::UnaryPlus, start_pos)) + } ('+', _) => return Some((Token::Plus, start_pos)), - ('-', '0'..='9') if !state.non_unary => negated = Some(start_pos), + ('-', '0'..='9') if !state.next_token_cannot_be_unary => negated = Some(start_pos), ('-', '0'..='9') => return Some((Token::Minus, start_pos)), ('-', '=') => { eat_next(stream, pos); @@ -1695,7 +1701,9 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::Reserved("--".into()), start_pos)); } - ('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)), + ('-', _) if !state.next_token_cannot_be_unary => { + return Some((Token::UnaryMinus, start_pos)) + } ('-', _) => return Some((Token::Minus, start_pos)), ('*', ')') => { @@ -2117,6 +2125,10 @@ impl InputStream for MultiInputsStream<'_> { /// _(internals)_ An iterator on a [`Token`] stream. /// Exported under the `internals` feature only. +/// +/// # Volatile Data Structure +/// +/// This type is volatile and may change. pub struct TokenIterator<'a> { /// Reference to the scripting `Engine`. pub engine: &'a Engine, @@ -2224,7 +2236,7 @@ impl<'a> Iterator for TokenIterator<'a> { // Run the mapper, if any let token = if let Some(map_func) = self.token_mapper { - map_func(token) + map_func(token, pos, &self.state) } else { token }; @@ -2278,7 +2290,7 @@ impl Engine { max_string_size: self.limits.max_string_size, #[cfg(feature = "unchecked")] max_string_size: None, - non_unary: false, + next_token_cannot_be_unary: false, comment_level: 0, include_comments: false, is_within_text_terminated_by: None, diff --git a/tests/get_set.rs b/tests/get_set.rs index ab0894a9..1b8468fa 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -1,6 +1,6 @@ #![cfg(not(feature = "no_object"))] -use rhai::{Engine, EvalAltResult, INT}; +use rhai::{Engine, EvalAltResult, Scope, INT}; #[test] fn test_get_set() -> Result<(), Box> { @@ -70,7 +70,7 @@ fn test_get_set() -> Result<(), Box> { } #[test] -fn test_get_set_chain() -> Result<(), Box> { +fn test_get_set_chain_with_write_back() -> Result<(), Box> { #[derive(Clone)] struct TestChild { x: INT, @@ -119,11 +119,22 @@ fn test_get_set_chain() -> Result<(), Box> { engine.register_get_set("x", TestChild::get_x, TestChild::set_x); engine.register_get_set("child", TestParent::get_child, TestParent::set_child); - engine.register_fn("new_tp", TestParent::new); + #[cfg(not(feature = "no_index"))] + engine.register_indexer_get_set( + |parent: &mut TestParent, _: INT| parent.child.clone(), + |parent: &mut TestParent, n: INT, mut new_child: TestChild| { + new_child.x *= n; + parent.child = new_child; + }, + ); + engine.register_fn("new_tp", TestParent::new); + engine.register_fn("new_tc", TestChild::new); + + assert_eq!(engine.eval::("let a = new_tp(); a.child.x")?, 1); assert_eq!( - engine.eval::("let a = new_tp(); a.child.x = 500; a.child.x")?, - 500 + engine.eval::("let a = new_tp(); a.child.x = 42; a.child.x")?, + 42 ); assert_eq!( @@ -131,6 +142,18 @@ fn test_get_set_chain() -> Result<(), Box> { "TestParent" ); + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval::("let a = new_tp(); let c = new_tc(); c.x = 123; a[2] = c; a.child.x")?, + 246 + ); + + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval::("let a = new_tp(); a[2].x = 42; a.child.x")?, + 84 + ); + Ok(()) } @@ -166,3 +189,68 @@ fn test_get_set_op_assignment() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_get_set_chain_without_write_back() -> Result<(), Box> { + #[derive(Debug, Clone)] + struct Outer { + pub inner: Inner, + } + + #[derive(Debug, Clone)] + struct Inner { + pub value: INT, + } + + let mut engine = Engine::new(); + let mut scope = Scope::new(); + + scope.push( + "outer", + Outer { + inner: Inner { value: 42 }, + }, + ); + + engine + .register_type::() + .register_get_set( + "value", + |t: &mut Inner| t.value, + |_: &mut Inner, new: INT| panic!("Inner::value setter called with {}", new), + ) + .register_type::() + .register_get_set( + "inner", + |t: &mut Outer| t.inner.clone(), + |_: &mut Outer, new: Inner| panic!("Outer::inner setter called with {:?}", new), + ); + + #[cfg(not(feature = "no_index"))] + engine.register_indexer_get_set( + |t: &mut Outer, n: INT| Inner { + value: t.inner.value * n, + }, + |_: &mut Outer, n: INT, new: Inner| { + panic!("Outer::inner index setter called with {} and {:?}", n, new) + }, + ); + + assert_eq!( + engine.eval_with_scope::(&mut scope, "outer.inner.value")?, + 42 + ); + + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval_with_scope::(&mut scope, "outer[2].value")?, + 84 + ); + + engine.run_with_scope(&mut scope, "print(outer.inner.value)")?; + + #[cfg(not(feature = "no_index"))] + engine.run_with_scope(&mut scope, "print(outer[0].value)")?; + + Ok(()) +}