From 5f40a1376aa594c33d3e2077e7079dc925de2ed7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 6 Jun 2020 13:06:00 +0800 Subject: [PATCH] Implement index setters. --- README.md | 59 +++++++------ RELEASES.md | 7 ++ src/api.rs | 107 +++++++++++++++++++++-- src/engine.rs | 220 ++++++++++++++++++++++++++++++----------------- src/module.rs | 48 +++++++++-- tests/get_set.rs | 19 ++-- 6 files changed, 339 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 7021b924..39539e54 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ The follow packages are available: 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). Once a package is created (e.g. via `new`), it can be _shared_ (via `get`) among multiple instances of [`Engine`], -even across threads (under the [`sync`] feature). Therefore, a package only has to be created _once_. +even across threads (under [`sync`]). Therefore, a package only has to be created _once_. Packages are actually implemented as [modules], so they share a lot of behavior and characteristics. The main difference is that a package loads under the _global_ namespace, while a module loads under its own @@ -572,7 +572,7 @@ if type_of(x) == "string" { [`Dynamic`]: #dynamic-values -A `Dynamic` value can be _any_ type. However, under the [`sync`] feature, all types must be `Send + Sync`. +A `Dynamic` value can be _any_ type. However, under [`sync`], all types must be `Send + Sync`. Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific actions based on the actual value's type. @@ -975,8 +975,8 @@ let result = engine.eval::( println!("result: {}", result); // prints 1 ``` -Under the [`no_object`] feature, however, the _method_ style of function calls -(i.e. calling a function as an object-method) is no longer supported. +Under [`no_object`], however, the _method_ style of function calls (i.e. calling a function as an object-method) +is no longer supported. ```rust // Below is a syntax error under 'no_object' because 'clear' cannot be called in method style. @@ -999,8 +999,7 @@ let x = new_ts(); print(x.type_of()); // prints "Hello" ``` -Getters and setters -------------------- +### Getters and setters Similarly, custom types can expose members by registering a `get` and/or `set` function. @@ -1040,12 +1039,10 @@ let result = engine.eval::(r#"let a = new_ts(); a.xyz = "42"; a.xyz"#)?; println!("Answer: {}", result); // prints 42 ``` -Indexers --------- +### Indexers Custom types can also expose an _indexer_ by registering an indexer function. 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 #[derive(Clone)] @@ -1057,9 +1054,12 @@ impl TestStruct { fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + fn set_field(&mut self, index: i64, value: i64) { + self.fields[index as usize] = value + } fn new() -> Self { - TestStruct { fields: vec![1, 2, 42, 4, 5] } + TestStruct { fields: vec![1, 2, 3, 4, 5] } } } @@ -1068,22 +1068,31 @@ let engine = Engine::new(); engine.register_type::(); engine.register_fn("new_ts", TestStruct::new); -engine.register_indexer(TestStruct::get_field); -let result = engine.eval::("let a = new_ts(); a[2]")?; +// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); +engine.register_indexer_get(TestStruct::get_field); +engine.register_indexer_set(TestStruct::set_field); + +let result = engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?; println!("Answer: {}", result); // prints 42 ``` -Needless to say, `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set` -and `register_indexer` are not available under the [`no_object`] feature. -`register_indexer` is also not available under the [`no_index`] feature. +For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for +[arrays] and [object maps]. -Printing for custom types -------------------------- +### Disabling custom types -To use custom types for `print` and `debug`, or format its value into a [string], it is necessary that the following -functions be registered (assuming the custom type is `T` and it is `Display + Debug`): +The custom types API `register_type`, `register_type_with_name`, `register_get`, `register_set`, `register_get_set`, +`register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are not available under [`no_object`]. + +The indexers API `register_indexer_get`, `register_indexer_set` and `register_indexer_get_set` are also +not available under [`no_index`]. + +### Printing for custom types + +To use custom types for `print` and `debug`, or convert its value into a [string], it is necessary that the following +functions be registered (assuming the custom type is `T : Display + Debug`): | Function | Signature | Typical implementation | Usage | | ----------- | ------------------------------------------------ | ------------------------------ | --------------------------------------------------------------------------------------- | @@ -1103,7 +1112,7 @@ By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting on but no global state. This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next, such a state must be manually created and passed in. -All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under the [`sync`] feature, however, +All `Scope` variables are [`Dynamic`], meaning they can store values of any type. Under [`sync`], however, only types that are `Send + Sync` are supported, and the entire `Scope` itself will also be `Send + Sync`. This is extremely useful in multi-threaded applications. @@ -1205,7 +1214,7 @@ The following are reserved keywords in Rhai: | `import`, `export`, `as` | Modules | [`no_module`] | Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. -For example, `fn` is a valid variable name under the [`no_function`] feature. +For example, `fn` is a valid variable name under [`no_function`]. Statements ---------- @@ -2375,10 +2384,10 @@ which simply loads a script file based on the path (with `.rhai` extension attac Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under the [`no_std`] feature. | +| Module Resolver | Description | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FileModuleResolver` | The default module resolution service, not available under [`no_std`]. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: diff --git a/RELEASES.md b/RELEASES.md index 7ba44391..58303958 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,6 +8,13 @@ Breaking changes ---------------- * Callback closure passed to `Engine::on_progress` now takes `&u64` instead of `u64` to be consistent with other callback signatures. +* `Engine::register_indexer` is renamed to `Engine::register_indexer_get`. +* `Module::set_indexer_fn` is renamed to `Module::set_indexer_get_fn`. + +New features +------------ + +* Indexers are now split into getters ans setters (which now support updates). The API is split into `Engine::register_indexer_get` and `Engine::register_indexer_set` with `Engine::register_indexer_get_set` being a shorthand. Similarly, `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn` are added. Version 0.15.0 diff --git a/src/api.rs b/src/api.rs index 6c7a6e7e..df92f891 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,7 +2,8 @@ use crate::any::{Dynamic, Variant}; use crate::engine::{ - get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER, + get_script_function_by_signature, make_getter, make_setter, Engine, State, FUNC_INDEXER_GET, + FUNC_INDEXER_SET, }; use crate::error::ParseError; use crate::fn_call::FuncArgs; @@ -275,7 +276,7 @@ impl Engine { self.register_set(name, set_fn); } - /// Register an indexer function for a registered type with the `Engine`. + /// Register an index getter for a registered type with the `Engine`. /// /// The function signature must start with `&mut self` and not `&self`. /// @@ -305,7 +306,7 @@ impl Engine { /// engine.register_fn("new_ts", TestStruct::new); /// /// // Register an indexer. - /// engine.register_indexer(TestStruct::get_field); + /// engine.register_indexer_get(TestStruct::get_field); /// /// assert_eq!(engine.eval::("let a = new_ts(); a[2]")?, 3); /// # Ok(()) @@ -313,7 +314,7 @@ impl Engine { /// ``` #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] - pub fn register_indexer( + pub fn register_indexer_get( &mut self, callback: impl Fn(&mut T, X) -> U + SendSync + 'static, ) where @@ -321,7 +322,103 @@ impl Engine { U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FUNC_INDEXER, callback); + self.register_fn(FUNC_INDEXER_GET, callback); + } + + /// Register an index setter for a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// fn set_field(&mut self, index: i64, value: i64) { self.fields[index as usize] = value; } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register an indexer. + /// engine.register_indexer_set(TestStruct::set_field); + /// + /// assert_eq!( + /// engine.eval::("let a = new_ts(); a[2] = 42; a")?.fields[2], + /// 42 + /// ); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer_set( + &mut self, + callback: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + ) where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + { + self.register_fn(FUNC_INDEXER_SET, callback); + } + + /// Shorthand for register both index getter and setter functions for a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + /// fn set_field(&mut self, index: i64, value: i64) { self.fields[index as usize] = value; } + /// } + /// + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register an indexer. + /// engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); + /// + /// assert_eq!(engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?, 42); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer_get_set( + &mut self, + getter: impl Fn(&mut T, X) -> U + SendSync + 'static, + setter: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + ) where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + { + self.register_indexer_get(getter); + self.register_indexer_set(setter); } /// Compile a string into an `AST`, which can be used later for evaluation. diff --git a/src/engine.rs b/src/engine.rs index b79c497b..c10a89d4 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -74,9 +74,11 @@ pub const KEYWORD_EVAL: &str = "eval"; pub const FUNC_TO_STRING: &str = "to_string"; pub const FUNC_GETTER: &str = "get$"; pub const FUNC_SETTER: &str = "set$"; -pub const FUNC_INDEXER: &str = "$index$"; +pub const FUNC_INDEXER_GET: &str = "$index$get$"; +pub const FUNC_INDEXER_SET: &str = "$index$set$"; /// A type that encapsulates a mutation target for an expression with side effects. +#[derive(Debug)] enum Target<'a> { /// The target is a mutable reference to a `Dynamic` value somewhere. Ref(&'a mut Dynamic), @@ -91,40 +93,45 @@ impl Target<'_> { /// 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, + Self::Ref(_) => true, + Self::Value(_) | Self::StringChar(_, _, _) => false, + } + } + /// Is the `Target` an owned value? + pub fn is_value(&self) -> bool { + match self { + Self::Ref(_) => false, + Self::Value(_) => true, + Self::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(), // Referenced value is cloned - Target::Value(v) => v, // Owned value is simply taken - Target::StringChar(_, _, ch) => ch, // Character is taken + Self::Ref(r) => r.clone(), // Referenced value is cloned + Self::Value(v) => v, // Owned value is simply taken + Self::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, + Self::Ref(r) => *r, + Self::Value(ref mut r) => r, + Self::StringChar(_, _, ref mut r) => r, } } - /// Update the value of the `Target`. /// Position in `EvalAltResult` is None and must be set afterwards. pub fn set_value(&mut self, new_val: Dynamic) -> Result<(), Box> { match self { - Target::Ref(r) => **r = new_val, - Target::Value(_) => { + Self::Ref(r) => **r = new_val, + Self::Value(_) => { return Err(Box::new(EvalAltResult::ErrorAssignmentToUnknownLHS( Position::none(), ))) } - Target::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { + Self::StringChar(Dynamic(Union::Str(ref mut s)), index, _) => { // Replace the character at the specified index position let new_ch = new_val .as_char() @@ -163,20 +170,17 @@ impl> From for Target<'_> { /// /// This type uses some unsafe code, mainly for avoiding cloning of local variable names via /// direct lifetime casting. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] pub struct State { /// Normally, access to variables are parsed with a relative offset into the scope to avoid a lookup. /// In some situation, e.g. after running an `eval` statement, subsequent offsets become mis-aligned. /// When that happens, this flag is turned on to force a scope lookup by name. pub always_search: bool, - /// Level of the current scope. The global (root) level is zero, a new block (or function call) /// is one level higher, and so on. pub scope_level: usize, - /// Number of operations performed. pub operations: u64, - /// Number of modules loaded. pub modules: u64, } @@ -361,14 +365,23 @@ fn search_scope<'s, 'a>( _ => unreachable!(), }; + // Check if it is qualified if let Some(modules) = modules.as_ref() { - let module = if let Some(index) = modules.index() { + // Qualified - check if the root module is directly indexed + let index = if state.always_search { + None + } else { + modules.index() + }; + + let module = if let Some(index) = index { scope .get_mut(scope.len() - index.get()) .0 .downcast_mut::() .unwrap() } else { + // Find the root module in the scope let (id, root_pos) = modules.get(0); scope @@ -376,28 +389,27 @@ fn search_scope<'s, 'a>( .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? }; - return Ok(( - module.get_qualified_var_mut(name, *hash_var, *pos)?, - name, - // Module variables are constant - ScopeEntryType::Constant, - *pos, - )); - } + let target = module.get_qualified_var_mut(name, *hash_var, *pos)?; - let index = if state.always_search { None } else { *index }; - - let index = if let Some(index) = index { - scope.len() - index.get() + // Module variables are constant + Ok((target, name, ScopeEntryType::Constant, *pos)) } else { - scope - .get_index(name) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? - .0 - }; + // Unqualified - check if it is directly indexed + let index = if state.always_search { None } else { *index }; - let (val, typ) = scope.get_mut(index); - Ok((val, name, typ, *pos)) + let index = if let Some(index) = index { + scope.len() - index.get() + } else { + // Find the variable in the scope + scope + .get_index(name) + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? + .0 + }; + + let (val, typ) = scope.get_mut(index); + Ok((val, name, typ, *pos)) + } } impl Engine { @@ -585,10 +597,16 @@ impl Engine { .or_else(|| self.packages.get_fn(hashes.0)); if let Some(func) = func { + // Calling pure function in method-call? + normalize_first_arg( + (func.is_pure() || func.is_script()) && is_ref, + &mut this_copy, + &mut old_this_ptr, + args, + ); + if func.is_script() { // Run scripted function - normalize_first_arg(is_ref, &mut this_copy, &mut old_this_ptr, args); - let fn_def = func.get_fn_def(); let result = self.call_script_fn(scope, state, lib, fn_name, fn_def, args, level)?; @@ -598,14 +616,6 @@ impl Engine { return Ok((result, false)); } else { - // Calling pure function in method-call? - normalize_first_arg( - func.is_pure() && is_ref, - &mut this_copy, - &mut old_this_ptr, - args, - ); - // Run external function let result = func.get_native_fn()(args)?; @@ -668,22 +678,40 @@ impl Engine { ))); } - let types_list: Vec<_> = args - .iter() - .map(|name| self.map_type_name(name.type_name())) - .collect(); - - // Getter function not found? - if fn_name == FUNC_INDEXER { + // index getter function not found? + if fn_name == FUNC_INDEXER_GET && args.len() == 2 { return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - format!("[]({})", types_list.join(", ")), + format!( + "{} [{}]", + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), + ), + Position::none(), + ))); + } + + // index setter function not found? + if fn_name == FUNC_INDEXER_SET { + return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + format!( + "{} [{}]=", + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), + ), Position::none(), ))); } // Raise error Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - format!("{} ({})", fn_name, types_list.join(", ")), + format!( + "{} ({})", + fn_name, + args.iter() + .map(|name| self.map_type_name(name.type_name())) + .collect::>() + .join(", ") + ), Position::none(), ))) } @@ -787,12 +815,8 @@ impl Engine { level: usize, ) -> Result<(Dynamic, bool), Box> { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. - let hash_fn = calc_fn_hash( - empty(), - fn_name, - args.len(), - args.iter().map(|a| a.type_id()), - ); + let arg_types = args.iter().map(|a| a.type_id()); + let hash_fn = calc_fn_hash(empty(), fn_name, args.len(), arg_types); let hashes = (hash_fn, if native_only { 0 } else { hash_fn_def }); match fn_name { @@ -890,8 +914,8 @@ impl Engine { let (idx, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); let idx_pos = idx.position(); - let this_ptr = &mut self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, idx_pos, false)?; + let this_ptr = + &mut self.get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; self.eval_dot_index_chain_helper( state, lib, this_ptr, expr, idx_values, is_idx, level, new_val, @@ -900,17 +924,52 @@ impl Engine { } // xxx[rhs] = new_val _ if new_val.is_some() => { - let this_ptr = - &mut self.get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, true)?; + let mut idx_val2 = idx_val.clone(); - this_ptr - .set_value(new_val.unwrap()) - .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; - Ok((Default::default(), true)) + match self.get_indexed_mut(state, lib, target, idx_val, pos, true) { + // Indexed value is an owned value - the only possibility is an indexer + // Try to call an index setter + Ok(this_ptr) if this_ptr.is_value() => { + let fn_name = FUNC_INDEXER_SET; + let args = &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + + self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) + .or_else(|err| match *err { + // If there is no index setter, no need to set it back because the indexer is read-only + EvalAltResult::ErrorFunctionNotFound(s, _) + if s == FUNC_INDEXER_SET => + { + Ok(Default::default()) + } + _ => Err(err), + })?; + } + // Indexed value is a reference - update directly + Ok(ref mut this_ptr) => { + this_ptr + .set_value(new_val.unwrap()) + .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; + } + Err(err) => match *err { + // No index getter - try to call an index setter + EvalAltResult::ErrorIndexingType(_, _) => { + let fn_name = FUNC_INDEXER_SET; + let args = + &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + + self.exec_fn_call( + state, lib, fn_name, true, 0, args, is_ref, None, 0, + )?; + } + // Error + err => return Err(Box::new(err)), + }, + } + Ok(Default::default()) } // xxx[rhs] _ => self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, false) + .get_indexed_mut(state, lib, target, idx_val, pos, false) .map(|v| (v.clone_into_dynamic(), false)), } } else { @@ -940,8 +999,7 @@ impl Engine { Expr::Property(x) if obj.is::() && new_val.is_some() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let mut val = - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, true)?; + let mut val = self.get_indexed_mut(state, lib, target, index, *pos, true)?; val.set_value(new_val.unwrap()) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; @@ -952,7 +1010,7 @@ impl Engine { Expr::Property(x) if obj.is::() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let val = self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, false)?; + let val = self.get_indexed_mut(state, lib, target, index, *pos, false)?; Ok((val.clone_into_dynamic(), false)) } @@ -981,7 +1039,7 @@ impl Engine { let mut val = if let Expr::Property(p) = prop { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); - self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, false)? + self.get_indexed_mut(state, lib, target, index, *pos, false)? } else { unreachable!(); }; @@ -1160,14 +1218,16 @@ impl Engine { &self, state: &mut State, lib: &Module, - val: &'a mut Dynamic, - is_ref: bool, + target: &'a mut Target, mut idx: Dynamic, idx_pos: Position, create: bool, ) -> Result, Box> { self.inc_operations(state)?; + let is_ref = target.is_ref(); + let val = target.as_mut(); + match val { #[cfg(not(feature = "no_index"))] Dynamic(Union::Array(arr)) => { @@ -1233,7 +1293,7 @@ impl Engine { } _ => { - let fn_name = FUNC_INDEXER; + let fn_name = FUNC_INDEXER_GET; let type_name = self.map_type_name(val.type_name()); let args = &mut [val, &mut idx]; self.exec_fn_call(state, lib, fn_name, true, 0, args, is_ref, None, 0) diff --git a/src/module.rs b/src/module.rs index d509f3c1..2a722e3b 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER}; +use crate::engine::{make_getter, make_setter, Engine, FUNC_INDEXER_GET, FUNC_INDEXER_SET}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, @@ -518,7 +518,7 @@ impl Module { self.set_fn_2_mut(make_setter(&name.into()), func) } - /// Set a Rust indexer function taking two parameters (the first one mutable) into the module, + /// Set a Rust index getter taking two parameters (the first one mutable) into the module, /// returning a hash key. /// /// If there is a similar existing setter Rust function, it is replaced. @@ -529,19 +529,19 @@ impl Module { /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_indexer_fn(|x: &mut i64, y: ImmutableString| { + /// let hash = module.set_indexer_get_fn(|x: &mut i64, y: ImmutableString| { /// Ok(*x + y.len() as i64) /// }); /// assert!(module.get_fn(hash).is_some()); /// ``` #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] - pub fn set_indexer_fn( + pub fn set_indexer_get_fn( &mut self, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, ) -> u64 { - self.set_fn_2_mut(FUNC_INDEXER, func) + self.set_fn_2_mut(FUNC_INDEXER_GET, func) } /// Set a Rust function taking three parameters into the module, returning a hash key. @@ -629,6 +629,44 @@ impl Module { ) } + /// Set a Rust index setter taking three parameters (the first one mutable) into the module, + /// returning a hash key. + /// + /// If there is a similar existing Rust function, it is replaced. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Module, ImmutableString}; + /// + /// let mut module = Module::new(); + /// let hash = module.set_indexer_set_fn(|x: &mut i64, y: ImmutableString, value: i64| { + /// *x = y.len() as i64 + value; + /// Ok(()) + /// }); + /// assert!(module.get_fn(hash).is_some()); + /// ``` + pub fn set_indexer_set_fn( + &mut self, + #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, A) -> FuncReturn<()> + 'static, + #[cfg(feature = "sync")] func: impl Fn(&mut A, B, A) -> FuncReturn<()> + Send + Sync + 'static, + ) -> u64 { + let f = move |args: &mut FnCallArgs| { + let b = mem::take(args[1]).cast::(); + let c = mem::take(args[2]).cast::(); + let a = args[0].downcast_mut::().unwrap(); + + func(a, b, c).map(Dynamic::from) + }; + let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; + self.set_fn( + FUNC_INDEXER_SET, + Public, + &args, + CallableFunction::from_method(Box::new(f)), + ) + } + /// Set a Rust function taking four parameters into the module, returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. diff --git a/tests/get_set.rs b/tests/get_set.rs index 2df59671..ca5c0c47 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -42,18 +42,25 @@ fn test_get_set() -> Result<(), Box> { engine.register_fn("add", |value: &mut INT| *value += 41); engine.register_fn("new_ts", TestStruct::new); - #[cfg(not(feature = "no_index"))] - engine.register_indexer(|value: &mut TestStruct, index: ImmutableString| { - 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::(r#"let a = new_ts(); a["abc"]"#)?, 4); + { + engine.register_indexer_get_set( + |value: &mut TestStruct, index: ImmutableString| value.array[index.len()], + |value: &mut TestStruct, index: ImmutableString, new_val: INT| { + value.array[index.len()] = new_val + }, + ); + assert_eq!(engine.eval::(r#"let a = new_ts(); a["abc"]"#)?, 4); + assert_eq!( + engine.eval::(r#"let a = new_ts(); a["abc"] = 42; a["abc"]"#)?, + 42 + ); + } Ok(()) }