Add support for custom type indexers.
This commit is contained in:
parent
798e1df298
commit
f081040767
37
README.md
37
README.md
@ -15,7 +15,7 @@ Rhai's current features set:
|
|||||||
|
|
||||||
* Easy-to-use language similar to JS+Rust
|
* Easy-to-use language similar to JS+Rust
|
||||||
* Easy integration with Rust [native functions](#working-with-functions) and [types](#custom-types-and-methods),
|
* 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
|
* Easily [call a script-defined function](#calling-rhai-functions-from-rust) from Rust
|
||||||
* Freely pass variables/constants into a script via an external [`Scope`]
|
* 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)
|
* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop)
|
||||||
@ -923,6 +923,41 @@ let result = engine.eval::<i64>("let a = new_ts(); a.xyz = 42; a.xyz")?;
|
|||||||
println!("Answer: {}", result); // prints 42
|
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<i64>
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<TestStruct>();
|
||||||
|
|
||||||
|
engine.register_fn("new_ts", TestStruct::new);
|
||||||
|
engine.register_indexer(TestStruct::get_field);
|
||||||
|
|
||||||
|
let result = engine.eval::<i64>("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`
|
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.
|
are not available when the [`no_object`] feature is turned on.
|
||||||
|
|
||||||
|
60
src/api.rs
60
src/api.rs
@ -1,7 +1,7 @@
|
|||||||
//! Module that defines the extern API of `Engine`.
|
//! Module that defines the extern API of `Engine`.
|
||||||
|
|
||||||
use crate::any::{Dynamic, Variant};
|
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::error::ParseError;
|
||||||
use crate::fn_call::FuncArgs;
|
use crate::fn_call::FuncArgs;
|
||||||
use crate::fn_register::RegisterFn;
|
use crate::fn_register::RegisterFn;
|
||||||
@ -42,6 +42,16 @@ pub trait ObjectSetCallback<T, U>: Fn(&mut T, U) + 'static {}
|
|||||||
#[cfg(not(feature = "sync"))]
|
#[cfg(not(feature = "sync"))]
|
||||||
impl<F: Fn(&mut T, U) + 'static, T, U> ObjectSetCallback<T, U> for F {}
|
impl<F: Fn(&mut T, U) + 'static, T, U> ObjectSetCallback<T, U> for F {}
|
||||||
|
|
||||||
|
#[cfg(feature = "sync")]
|
||||||
|
pub trait ObjectIndexerCallback<T, X, U>: Fn(&mut T, X) -> U + Send + Sync + 'static {}
|
||||||
|
#[cfg(feature = "sync")]
|
||||||
|
impl<F: Fn(&mut T, X) -> U + Send + Sync + 'static, T, X, U> ObjectIndexerCallback<T, X, U> for F {}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "sync"))]
|
||||||
|
pub trait ObjectIndexerCallback<T, X, U>: Fn(&mut T, X) -> U + 'static {}
|
||||||
|
#[cfg(not(feature = "sync"))]
|
||||||
|
impl<F: Fn(&mut T, X) -> U + 'static, T, X, U> ObjectIndexerCallback<T, X, U> for F {}
|
||||||
|
|
||||||
#[cfg(feature = "sync")]
|
#[cfg(feature = "sync")]
|
||||||
pub trait IteratorCallback:
|
pub trait IteratorCallback:
|
||||||
Fn(Dynamic) -> Box<dyn Iterator<Item = Dynamic>> + Send + Sync + 'static
|
Fn(Dynamic) -> Box<dyn Iterator<Item = Dynamic>> + Send + Sync + 'static
|
||||||
@ -299,6 +309,54 @@ impl Engine {
|
|||||||
self.register_set(name, set_fn);
|
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<i64>
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// 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<rhai::EvalAltResult>> {
|
||||||
|
/// use rhai::{Engine, RegisterFn};
|
||||||
|
///
|
||||||
|
/// let mut engine = Engine::new();
|
||||||
|
///
|
||||||
|
/// // Register the custom type.
|
||||||
|
/// engine.register_type::<TestStruct>();
|
||||||
|
///
|
||||||
|
/// engine.register_fn("new_ts", TestStruct::new);
|
||||||
|
///
|
||||||
|
/// // Register an indexer.
|
||||||
|
/// engine.register_indexer(TestStruct::get_field);
|
||||||
|
///
|
||||||
|
/// assert_eq!(engine.eval::<i64>("let a = new_ts(); a[2]")?, 3);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[cfg(not(feature = "no_object"))]
|
||||||
|
#[cfg(not(feature = "no_index"))]
|
||||||
|
pub fn register_indexer<T, X, U, F>(&mut self, callback: F)
|
||||||
|
where
|
||||||
|
T: Variant + Clone,
|
||||||
|
U: Variant + Clone,
|
||||||
|
X: Variant + Clone,
|
||||||
|
F: ObjectIndexerCallback<T, X, U>,
|
||||||
|
{
|
||||||
|
self.register_fn(FUNC_INDEXER, callback);
|
||||||
|
}
|
||||||
|
|
||||||
/// Compile a string into an `AST`, which can be used later for evaluation.
|
/// Compile a string into an `AST`, which can be used later for evaluation.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -74,6 +74,7 @@ pub const KEYWORD_EVAL: &str = "eval";
|
|||||||
pub const FUNC_TO_STRING: &str = "to_string";
|
pub const FUNC_TO_STRING: &str = "to_string";
|
||||||
pub const FUNC_GETTER: &str = "get$";
|
pub const FUNC_GETTER: &str = "get$";
|
||||||
pub const FUNC_SETTER: &str = "set$";
|
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.
|
/// A type that encapsulates a mutation target for an expression with side effects.
|
||||||
enum Target<'a> {
|
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?
|
// Getter function not found?
|
||||||
if let Some(prop) = extract_prop_from_getter(fn_name) {
|
if let Some(prop) = extract_prop_from_getter(fn_name) {
|
||||||
return Err(Box::new(EvalAltResult::ErrorDotExpr(
|
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
|
let types_list: Vec<_> = args
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| self.map_type_name(name.type_name()))
|
.map(|name| self.map_type_name(name.type_name()))
|
||||||
.collect();
|
.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(
|
Err(Box::new(EvalAltResult::ErrorFunctionNotFound(
|
||||||
format!("{} ({})", fn_name, types_list.join(", ")),
|
format!("{} ({})", fn_name, types_list.join(", ")),
|
||||||
pos,
|
pos,
|
||||||
@ -787,20 +796,20 @@ impl Engine {
|
|||||||
Expr::Index(idx, idx_rhs, pos) => {
|
Expr::Index(idx, idx_rhs, pos) => {
|
||||||
let is_index = matches!(rhs, Expr::Index(_,_,_));
|
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(
|
self.eval_dot_index_chain_helper(
|
||||||
fn_lib, indexed_val, idx_rhs.as_ref(), idx_values, is_index, *pos, level, new_val
|
fn_lib, indexed_val, idx_rhs.as_ref(), idx_values, is_index, *pos, level, new_val
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// xxx[rhs] = new_val
|
// xxx[rhs] = new_val
|
||||||
_ if new_val.is_some() => {
|
_ 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())?;
|
indexed_val.set_value(new_val.unwrap(), rhs.position())?;
|
||||||
Ok((Default::default(), true))
|
Ok((Default::default(), true))
|
||||||
}
|
}
|
||||||
// xxx[rhs]
|
// xxx[rhs]
|
||||||
_ => self
|
_ => 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))
|
.map(|v| (v.clone_into_dynamic(), false))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -818,14 +827,14 @@ impl Engine {
|
|||||||
// {xxx:map}.id = ???
|
// {xxx:map}.id = ???
|
||||||
Expr::Property(id, pos) if obj.is::<Map>() && new_val.is_some() => {
|
Expr::Property(id, pos) if obj.is::<Map>() && new_val.is_some() => {
|
||||||
let mut indexed_val =
|
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())?;
|
indexed_val.set_value(new_val.unwrap(), rhs.position())?;
|
||||||
Ok((Default::default(), true))
|
Ok((Default::default(), true))
|
||||||
}
|
}
|
||||||
// {xxx:map}.id
|
// {xxx:map}.id
|
||||||
Expr::Property(id, pos) if obj.is::<Map>() => {
|
Expr::Property(id, pos) if obj.is::<Map>() => {
|
||||||
let indexed_val =
|
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))
|
Ok((indexed_val.clone_into_dynamic(), false))
|
||||||
}
|
}
|
||||||
// xxx.id = ???
|
// xxx.id = ???
|
||||||
@ -847,7 +856,7 @@ impl Engine {
|
|||||||
let is_index = matches!(rhs, Expr::Index(_,_,_));
|
let is_index = matches!(rhs, Expr::Index(_,_,_));
|
||||||
|
|
||||||
let indexed_val = if let Expr::Property(id, pos) = dot_lhs.as_ref() {
|
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 {
|
} else {
|
||||||
// Syntax error
|
// Syntax error
|
||||||
return Err(Box::new(EvalAltResult::ErrorDotExpr(
|
return Err(Box::new(EvalAltResult::ErrorDotExpr(
|
||||||
@ -1010,8 +1019,9 @@ impl Engine {
|
|||||||
/// Get the value at the indexed position of a base type
|
/// Get the value at the indexed position of a base type
|
||||||
fn get_indexed_mut<'a>(
|
fn get_indexed_mut<'a>(
|
||||||
&self,
|
&self,
|
||||||
|
fn_lib: &FunctionsLib,
|
||||||
val: &'a mut Dynamic,
|
val: &'a mut Dynamic,
|
||||||
idx: Dynamic,
|
mut idx: Dynamic,
|
||||||
idx_pos: Position,
|
idx_pos: Position,
|
||||||
op_pos: Position,
|
op_pos: Position,
|
||||||
create: bool,
|
create: bool,
|
||||||
@ -1084,11 +1094,18 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
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
|
// Error - cannot be indexed
|
||||||
_ => Err(Box::new(EvalAltResult::ErrorIndexingType(
|
|
||||||
type_name.to_string(),
|
type_name.to_string(),
|
||||||
op_pos,
|
op_pos,
|
||||||
))),
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ pub enum EvalAltResult {
|
|||||||
/// String indexing out-of-bounds.
|
/// String indexing out-of-bounds.
|
||||||
/// Wrapped values are the current number of characters in the string and the index number.
|
/// Wrapped values are the current number of characters in the string and the index number.
|
||||||
ErrorStringBounds(usize, INT, Position),
|
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),
|
ErrorIndexingType(String, Position),
|
||||||
/// Trying to index into an array or string with an index that is not `i64`.
|
/// Trying to index into an array or string with an index that is not `i64`.
|
||||||
ErrorNumericIndexExpr(Position),
|
ErrorNumericIndexExpr(Position),
|
||||||
@ -104,7 +104,7 @@ impl EvalAltResult {
|
|||||||
}
|
}
|
||||||
Self::ErrorStringIndexExpr(_) => "Indexing into an object map expects a string index",
|
Self::ErrorStringIndexExpr(_) => "Indexing into an object map expects a string index",
|
||||||
Self::ErrorIndexingType(_, _) => {
|
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 => {
|
Self::ErrorArrayBounds(_, index, _) if *index < 0 => {
|
||||||
"Array access expects non-negative index"
|
"Array access expects non-negative index"
|
||||||
|
@ -8,6 +8,7 @@ fn test_get_set() -> Result<(), Box<EvalAltResult>> {
|
|||||||
struct TestStruct {
|
struct TestStruct {
|
||||||
x: INT,
|
x: INT,
|
||||||
y: INT,
|
y: INT,
|
||||||
|
array: Vec<INT>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestStruct {
|
impl TestStruct {
|
||||||
@ -24,7 +25,11 @@ fn test_get_set() -> Result<(), Box<EvalAltResult>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new() -> Self {
|
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<EvalAltResult>> {
|
|||||||
engine.register_fn("add", |value: &mut INT| *value += 41);
|
engine.register_fn("add", |value: &mut INT| *value += 41);
|
||||||
engine.register_fn("new_ts", TestStruct::new);
|
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::<INT>("let a = new_ts(); a.x = 500; a.x")?, 500);
|
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x = 500; a.x")?, 500);
|
||||||
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x.add(); a.x")?, 42);
|
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x.add(); a.x")?, 42);
|
||||||
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.y.add(); a.y")?, 0);
|
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.y.add(); a.y")?, 0);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "no_index"))]
|
||||||
|
assert_eq!(engine.eval::<INT>("let a = new_ts(); a[3]")?, 4);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user