diff --git a/README.md b/README.md index b453b04c..55b7b1c3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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), - including [getter/setter](#getters-and-setters)/[methods](#members-and-methods) + including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers) * 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) @@ -923,6 +923,41 @@ let result = engine.eval::("let a = new_ts(); a.xyz = 42; a.xyz")?; println!("Answer: {}", result); // prints 42 ``` +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 +(but not update it - indexers are read-only). + +```rust +#[derive(Clone)] +struct TestStruct { + fields: Vec +} + +impl TestStruct { + fn get_field(&mut self, index: i64) -> i64 { + self.field + } + + fn new() -> Self { + TestStruct { field: vec![1, 2, 42, 4, 5] } + } +} + +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]")?; + +println!("Answer: {}", result); // prints 42 +``` + Needless to say, `register_type`, `register_type_with_name`, `register_get`, `register_set` and `register_get_set` are not available when the [`no_object`] feature is turned on. diff --git a/src/api.rs b/src/api.rs index 9377bc68..addcbe45 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Dynamic, Variant}; -use crate::engine::{make_getter, make_setter, Engine, Map, State}; +use crate::engine::{make_getter, make_setter, Engine, Map, State, FUNC_INDEXER}; use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_register::RegisterFn; @@ -42,6 +42,16 @@ pub trait ObjectSetCallback: Fn(&mut T, U) + 'static {} #[cfg(not(feature = "sync"))] impl ObjectSetCallback for F {} +#[cfg(feature = "sync")] +pub trait ObjectIndexerCallback: Fn(&mut T, X) -> U + Send + Sync + 'static {} +#[cfg(feature = "sync")] +impl U + Send + Sync + 'static, T, X, U> ObjectIndexerCallback for F {} + +#[cfg(not(feature = "sync"))] +pub trait ObjectIndexerCallback: Fn(&mut T, X) -> U + 'static {} +#[cfg(not(feature = "sync"))] +impl U + 'static, T, X, U> ObjectIndexerCallback for F {} + #[cfg(feature = "sync")] pub trait IteratorCallback: Fn(Dynamic) -> Box> + Send + Sync + 'static @@ -299,6 +309,54 @@ impl Engine { self.register_set(name, set_fn); } + /// Register an indexer function for a registered type with the `Engine`. + /// + /// The function signature must start with `&mut self` and not `&self`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// fields: Vec + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { fields: vec![1, 2, 3, 4, 5] } } + /// + /// // Even a getter must start with `&mut self` and not `&self`. + /// fn get_field(&mut self, index: i64) -> i64 { self.fields[index as usize] } + /// } + /// + /// # 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(TestStruct::get_field); + /// + /// assert_eq!(engine.eval::("let a = new_ts(); a[2]")?, 3); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn register_indexer(&mut self, callback: F) + where + T: Variant + Clone, + U: Variant + Clone, + X: Variant + Clone, + F: ObjectIndexerCallback, + { + self.register_fn(FUNC_INDEXER, callback); + } + /// Compile a string into an `AST`, which can be used later for evaluation. /// /// # Example diff --git a/src/engine.rs b/src/engine.rs index 7e849c7a..ae9cd8d2 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -74,6 +74,7 @@ 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$"; /// A type that encapsulates a mutation target for an expression with side effects. enum Target<'a> { @@ -577,6 +578,11 @@ impl Engine { }); } + // Return default value (if any) + if let Some(val) = def_val { + return Ok(val.clone()); + } + // Getter function not found? if let Some(prop) = extract_prop_from_getter(fn_name) { return Err(Box::new(EvalAltResult::ErrorDotExpr( @@ -593,17 +599,20 @@ impl Engine { ))); } - // Return default value (if any) - if let Some(val) = def_val { - return Ok(val.clone()); - } - - // Raise error 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 { + return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + format!("[]({})", types_list.join(", ")), + pos, + ))); + } + + // Raise error Err(Box::new(EvalAltResult::ErrorFunctionNotFound( format!("{} ({})", fn_name, types_list.join(", ")), pos, @@ -787,20 +796,20 @@ impl Engine { Expr::Index(idx, idx_rhs, pos) => { let is_index = matches!(rhs, Expr::Index(_,_,_)); - let indexed_val = self.get_indexed_mut(obj, idx_val, idx.position(), op_pos, false)?; + let indexed_val = self.get_indexed_mut(fn_lib, obj, idx_val, idx.position(), op_pos, false)?; self.eval_dot_index_chain_helper( fn_lib, indexed_val, idx_rhs.as_ref(), idx_values, is_index, *pos, level, new_val ) } // xxx[rhs] = new_val _ if new_val.is_some() => { - let mut indexed_val = self.get_indexed_mut(obj, idx_val, rhs.position(), op_pos, true)?; + let mut indexed_val = self.get_indexed_mut(fn_lib, obj, idx_val, rhs.position(), op_pos, true)?; indexed_val.set_value(new_val.unwrap(), rhs.position())?; Ok((Default::default(), true)) } // xxx[rhs] _ => self - .get_indexed_mut(obj, idx_val, rhs.position(), op_pos, false) + .get_indexed_mut(fn_lib, obj, idx_val, rhs.position(), op_pos, false) .map(|v| (v.clone_into_dynamic(), false)) } } else { @@ -818,14 +827,14 @@ impl Engine { // {xxx:map}.id = ??? Expr::Property(id, pos) if obj.is::() && new_val.is_some() => { let mut indexed_val = - self.get_indexed_mut(obj, id.to_string().into(), *pos, op_pos, true)?; + self.get_indexed_mut(fn_lib, obj, id.to_string().into(), *pos, op_pos, true)?; indexed_val.set_value(new_val.unwrap(), rhs.position())?; Ok((Default::default(), true)) } // {xxx:map}.id Expr::Property(id, pos) if obj.is::() => { let indexed_val = - self.get_indexed_mut(obj, id.to_string().into(), *pos, op_pos, false)?; + self.get_indexed_mut(fn_lib, obj, id.to_string().into(), *pos, op_pos, false)?; Ok((indexed_val.clone_into_dynamic(), false)) } // xxx.id = ??? @@ -847,7 +856,7 @@ impl Engine { let is_index = matches!(rhs, Expr::Index(_,_,_)); let indexed_val = if let Expr::Property(id, pos) = dot_lhs.as_ref() { - self.get_indexed_mut(obj, id.to_string().into(), *pos, op_pos, false)? + self.get_indexed_mut(fn_lib, obj, id.to_string().into(), *pos, op_pos, false)? } else { // Syntax error return Err(Box::new(EvalAltResult::ErrorDotExpr( @@ -1010,8 +1019,9 @@ impl Engine { /// Get the value at the indexed position of a base type fn get_indexed_mut<'a>( &self, + fn_lib: &FunctionsLib, val: &'a mut Dynamic, - idx: Dynamic, + mut idx: Dynamic, idx_pos: Position, op_pos: Position, create: bool, @@ -1084,11 +1094,18 @@ impl Engine { } } - // Error - cannot be indexed - _ => Err(Box::new(EvalAltResult::ErrorIndexingType( - type_name.to_string(), - op_pos, - ))), + _ => { + let args = &mut [val, &mut idx]; + self.exec_fn_call(fn_lib, FUNC_INDEXER, args, None, op_pos, 0) + .map(|v| v.into()) + .map_err(|_| { + Box::new(EvalAltResult::ErrorIndexingType( + // Error - cannot be indexed + type_name.to_string(), + op_pos, + )) + }) + } } } diff --git a/src/result.rs b/src/result.rs index 17fe389a..c88342ee 100644 --- a/src/result.rs +++ b/src/result.rs @@ -47,7 +47,7 @@ pub enum EvalAltResult { /// String indexing out-of-bounds. /// Wrapped values are the current number of characters in the string and the index number. ErrorStringBounds(usize, INT, Position), - /// Trying to index into a type that is not an array, an object map, or a string. + /// Trying to index into a type that is not an array, an object map, or a string, and has no indexer function defined. ErrorIndexingType(String, Position), /// Trying to index into an array or string with an index that is not `i64`. ErrorNumericIndexExpr(Position), @@ -104,7 +104,7 @@ impl EvalAltResult { } Self::ErrorStringIndexExpr(_) => "Indexing into an object map expects a string index", Self::ErrorIndexingType(_, _) => { - "Indexing can only be performed on an array, an object map, or a string" + "Indexing can only be performed on an array, an object map, a string, or a type with an indexer function defined" } Self::ErrorArrayBounds(_, index, _) if *index < 0 => { "Array access expects non-negative index" diff --git a/tests/get_set.rs b/tests/get_set.rs index 633192ad..57712304 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -8,6 +8,7 @@ fn test_get_set() -> Result<(), Box> { struct TestStruct { x: INT, y: INT, + array: Vec, } impl TestStruct { @@ -24,7 +25,11 @@ fn test_get_set() -> Result<(), Box> { } fn new() -> Self { - TestStruct { x: 1, y: 0 } + TestStruct { + x: 1, + y: 0, + array: vec![1, 2, 3, 4, 5], + } } } @@ -37,10 +42,16 @@ 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: INT| value.array[index as usize]); + 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); + Ok(()) }