Add object maps.

This commit is contained in:
Stephen Chung 2020-03-29 23:53:35 +08:00
parent ef6c6ea6d2
commit 45ee51874f
9 changed files with 632 additions and 202 deletions

View File

@ -272,6 +272,7 @@ The following primitive types are supported natively:
| **Unicode character** | `char` | `"char"` |
| **Unicode string** | `String` (_not_ `&str`) | `"string"` |
| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` |
| **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` |
| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ |
| **System number** (current configuration) | `rhai::INT` (`i32` or `i64`),<br/>`rhai::FLOAT` (`f32` or `f64`) | _same as type_ |
| **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` |
@ -999,7 +1000,7 @@ let foo = [1, 2, 3][0];
foo == 1;
fn abc() {
[42, 43, 44] // a function returning an array literal
[42, 43, 44] // a function returning an array
}
let foo = abc()[0];
@ -1040,6 +1041,68 @@ print(y.len()); // prints 0
engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(item)) );
```
Object maps
-----------
Object maps are dictionaries. Properties of any type (`Dynamic`) can be freely added and retrieved.
Object map literals are built within braces '`${`' ... '`}`' (_name_ `:` _value_ syntax similar to Rust)
and separated by commas '`,`'.
The Rust type of a Rhai object map is `rhai::Map`.
[`type_of()`] an object map returns `"map"`.
Object maps are disabled via the [`no_object`] feature.
The following functions (defined in the standard library but excluded if [`no_stdlib`]) operate on object maps:
| Function | Description |
| -------- | ------------------------------------------------------------ |
| `has` | does the object map contain a property of a particular name? |
| `len` | returns the number of properties |
| `clear` | empties the object map |
Examples:
```rust
let y = ${ // object map literal with 3 properties
a: 1,
bar: "hello",
baz: 123.456
};
y.a = 42;
print(y.a); // prints 42
print(y["bar"]); // prints "hello" - access via string index
ts.obj = y; // object maps can be assigned completely (by value copy)
let foo = ts.list.a;
foo == 42;
let foo = ${ a:1, b:2, c:3 }["a"];
foo == 1;
fn abc() {
${ a:1, b:2, c:3 } // a function returning an object map
}
let foo = abc().b;
foo == 2;
let foo = y["a"];
foo == 42;
y.has("a") == true;
y.has("xyz") == false;
print(y.len()); // prints 3
y.clear(); // empty the object map
print(y.len()); // prints 0
```
Comparison operators
--------------------

View File

@ -10,6 +10,9 @@ use crate::result::EvalAltResult;
#[cfg(not(feature = "no_index"))]
use crate::engine::Array;
#[cfg(not(feature = "no_object"))]
use crate::engine::Map;
#[cfg(not(feature = "no_float"))]
use crate::parser::FLOAT;
@ -596,14 +599,20 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))]
{
reg_fn1!(self, "print", debug, String, Array);
reg_fn1!(self, "debug", debug, String, Array);
self.register_fn("print", |x: &mut Array| -> String { format!("{:?}", x) });
self.register_fn("debug", |x: &mut Array| -> String { format!("{:?}", x) });
// Register array iterator
self.register_iterator::<Array, _>(|a| {
Box::new(a.downcast_ref::<Array>().unwrap().clone().into_iter())
});
}
#[cfg(not(feature = "no_object"))]
{
self.register_fn("print", |x: &mut Map| -> String { format!("${:?}", x) });
self.register_fn("debug", |x: &mut Map| -> String { format!("${:?}", x) });
}
}
// Register range function
@ -822,6 +831,14 @@ impl Engine<'_> {
});
}
// Register map functions
#[cfg(not(feature = "no_object"))]
{
self.register_fn("has", |map: &mut Map, prop: String| map.contains_key(&prop));
self.register_fn("len", |map: &mut Map| map.len() as INT);
self.register_fn("clear", |map: &mut Map| map.clear());
}
// Register string concatenate functions
fn prepend<T: Display>(x: T, y: String) -> String {
format!("{}{}", x, y)

View File

@ -26,6 +26,10 @@ use crate::stdlib::{
#[cfg(not(feature = "no_index"))]
pub type Array = Vec<Dynamic>;
/// An dynamic hash map of `Dynamic` values.
#[cfg(not(feature = "no_object"))]
pub type Map = HashMap<String, Dynamic>;
pub type FnCallArgs<'a> = [&'a mut Variant];
pub type FnAny = dyn Fn(&mut FnCallArgs, Position) -> Result<Dynamic, EvalAltResult>;
@ -45,6 +49,8 @@ pub(crate) const FUNC_SETTER: &str = "set$";
#[cfg(not(feature = "no_index"))]
enum IndexSourceType {
Array,
#[cfg(not(feature = "no_object"))]
Map,
String,
Expression,
}
@ -156,6 +162,8 @@ impl Default for Engine<'_> {
let type_names = [
#[cfg(not(feature = "no_index"))]
(type_name::<Array>(), "array"),
#[cfg(not(feature = "no_object"))]
(type_name::<Map>(), "map"),
(type_name::<String>(), "string"),
(type_name::<Dynamic>(), "dynamic"),
]
@ -297,6 +305,17 @@ impl Engine<'_> {
}
if fn_name.starts_with(FUNC_GETTER) {
#[cfg(not(feature = "no_object"))]
{
// Map property access
if let Some(map) = args[0].downcast_ref::<Map>() {
return Ok(map
.get(&fn_name[FUNC_GETTER.len()..])
.cloned()
.unwrap_or_else(|| ().into_dynamic()));
}
}
// Getter function not found
return Err(EvalAltResult::ErrorDotExpr(
format!(
@ -308,6 +327,17 @@ impl Engine<'_> {
}
if fn_name.starts_with(FUNC_SETTER) {
#[cfg(not(feature = "no_object"))]
{
let val = args[1].into_dynamic();
// Map property update
if let Some(map) = args[0].downcast_mut::<Map>() {
map.insert(fn_name[FUNC_SETTER.len()..].to_string(), val);
return Ok(().into_dynamic());
}
}
// Setter function not found
return Err(EvalAltResult::ErrorDotExpr(
format!(
@ -398,21 +428,17 @@ impl Engine<'_> {
// xxx.idx_lhs[idx_expr]
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (val, _) = match idx_lhs.as_ref() {
let val = match idx_lhs.as_ref() {
// xxx.id[idx_expr]
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
let this_ptr = get_this_ptr(scope, src, target);
(
self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0)?,
*pos,
)
self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0)?
}
// xxx.???[???][idx_expr]
Expr::Index(_, _, _) => (
self.get_dot_val_helper(scope, src, target, idx_lhs, level)?,
*op_pos,
),
Expr::Index(_, _, _) => {
self.get_dot_val_helper(scope, src, target, idx_lhs, level)?
}
// Syntax error
_ => {
return Err(EvalAltResult::ErrorDotExpr(
@ -422,9 +448,8 @@ impl Engine<'_> {
}
};
let idx = self.eval_index_value(scope, idx_expr, level)?;
self.get_indexed_value(&val, idx, idx_expr.position(), *op_pos)
.map(|(v, _)| v)
self.get_indexed_value(scope, &val, idx_expr, *op_pos, level)
.map(|(v, _, _)| v)
}
// xxx.dot_lhs.rhs
@ -442,21 +467,17 @@ impl Engine<'_> {
// xxx.idx_lhs[idx_expr].rhs
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (val, _) = match idx_lhs.as_ref() {
let val = match idx_lhs.as_ref() {
// xxx.id[idx_expr].rhs
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
let this_ptr = get_this_ptr(scope, src, target);
(
self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0)?,
*pos,
)
self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0)?
}
// xxx.???[???][idx_expr].rhs
Expr::Index(_, _, _) => (
self.get_dot_val_helper(scope, src, target, idx_lhs, level)?,
*op_pos,
),
Expr::Index(_, _, _) => {
self.get_dot_val_helper(scope, src, target, idx_lhs, level)?
}
// Syntax error
_ => {
return Err(EvalAltResult::ErrorDotExpr(
@ -466,9 +487,8 @@ impl Engine<'_> {
}
};
let idx = self.eval_index_value(scope, idx_expr, level)?;
self.get_indexed_value(&val, idx, idx_expr.position(), *op_pos)
.and_then(|(mut v, _)| {
self.get_indexed_value(scope, &val, idx_expr, *op_pos, level)
.and_then(|(mut v, _, _)| {
self.get_dot_val_helper(scope, None, Some(v.as_mut()), rhs, level)
})
}
@ -499,7 +519,7 @@ impl Engine<'_> {
match dot_lhs {
// id.???
Expr::Variable(id, pos) => {
let (entry, _) = Self::search_scope(scope, id, Ok, *pos)?;
let (entry, _) = Self::search_scope(scope, id, *pos)?;
// Avoid referencing scope which is used below as mut
let entry = ScopeSource { name: id, ..entry };
@ -512,7 +532,7 @@ impl Engine<'_> {
// idx_lhs[idx_expr].???
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (src_type, src, idx, mut target) =
let (idx_src_type, src, idx, mut target) =
self.eval_index_expr(scope, idx_lhs, idx_expr, *op_pos, level)?;
let this_ptr = target.as_mut();
let val = self.get_dot_val_helper(scope, None, Some(this_ptr), dot_rhs, level);
@ -528,7 +548,7 @@ impl Engine<'_> {
}
ScopeEntryType::Normal => {
Self::update_indexed_var_in_scope(
src_type,
idx_src_type,
scope,
src,
idx,
@ -551,61 +571,85 @@ impl Engine<'_> {
}
/// Search for a variable within the scope, returning its value and index inside the Scope
fn search_scope<'a, T>(
fn search_scope<'a>(
scope: &'a Scope,
id: &str,
convert: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>,
begin: Position,
) -> Result<(ScopeSource<'a>, T), EvalAltResult> {
) -> Result<(ScopeSource<'a>, Dynamic), EvalAltResult> {
scope
.get(id)
.ok_or_else(|| EvalAltResult::ErrorVariableNotFound(id.into(), begin))
.and_then(move |(src, value)| convert(value).map(|v| (src, v)))
}
/// Evaluate the value of an index (must evaluate to INT)
#[cfg(not(feature = "no_index"))]
fn eval_index_value(
&mut self,
scope: &mut Scope,
idx_expr: &Expr,
level: usize,
) -> Result<INT, EvalAltResult> {
self.eval_expr(scope, idx_expr, level)?
.downcast::<INT>()
.map(|v| *v)
.map_err(|_| EvalAltResult::ErrorIndexExpr(idx_expr.position()))
}
/// Get the value at the indexed position of a base type
#[cfg(not(feature = "no_index"))]
fn get_indexed_value(
&self,
&mut self,
scope: &mut Scope,
val: &Dynamic,
idx: INT,
idx_pos: Position,
idx_expr: &Expr,
op_pos: Position,
) -> Result<(Dynamic, IndexSourceType), EvalAltResult> {
level: usize,
) -> Result<(Dynamic, IndexSourceType, (Option<usize>, Option<String>)), EvalAltResult> {
let idx_pos = idx_expr.position();
if val.is::<Array>() {
// val_array[idx]
let arr = val.downcast_ref::<Array>().expect("array expected");
if idx >= 0 {
let idx = *self
.eval_expr(scope, idx_expr, level)?
.downcast::<INT>()
.map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?;
return if idx >= 0 {
arr.get(idx as usize)
.cloned()
.map(|v| (v, IndexSourceType::Array))
.map(|v| (v, IndexSourceType::Array, (Some(idx as usize), None)))
.ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos))
} else {
Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos))
};
}
#[cfg(not(feature = "no_object"))]
{
if val.is::<Map>() {
// val_map[idx]
let map = val.downcast_ref::<Map>().expect("array expected");
let idx = *self
.eval_expr(scope, idx_expr, level)?
.downcast::<String>()
.map_err(|_| EvalAltResult::ErrorStringIndexExpr(idx_expr.position()))?;
return Ok((
map.get(&idx).cloned().unwrap_or_else(|| ().into_dynamic()),
IndexSourceType::Map,
(None, Some(idx)),
));
}
} else if val.is::<String>() {
}
if val.is::<String>() {
// val_string[idx]
let s = val.downcast_ref::<String>().expect("string expected");
if idx >= 0 {
let idx = *self
.eval_expr(scope, idx_expr, level)?
.downcast::<INT>()
.map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?;
return if idx >= 0 {
s.chars()
.nth(idx as usize)
.map(|ch| (ch.into_dynamic(), IndexSourceType::String))
.map(|ch| {
(
ch.into_dynamic(),
IndexSourceType::String,
(Some(idx as usize), None),
)
})
.ok_or_else(|| {
EvalAltResult::ErrorStringBounds(s.chars().count(), idx, idx_pos)
})
@ -615,14 +659,14 @@ impl Engine<'_> {
idx,
idx_pos,
))
}
} else {
// Error - cannot be indexed
Err(EvalAltResult::ErrorIndexingType(
self.map_type_name(val.type_name()).to_string(),
op_pos,
))
};
}
// Error - cannot be indexed
return Err(EvalAltResult::ErrorIndexingType(
self.map_type_name(val.type_name()).to_string(),
op_pos,
));
}
/// Evaluate an index expression
@ -634,36 +678,48 @@ impl Engine<'_> {
idx_expr: &Expr,
op_pos: Position,
level: usize,
) -> Result<(IndexSourceType, Option<ScopeSource<'a>>, usize, Dynamic), EvalAltResult> {
let idx = self.eval_index_value(scope, idx_expr, level)?;
) -> Result<
(
IndexSourceType,
Option<ScopeSource<'a>>,
(Option<usize>, Option<String>),
Dynamic,
),
EvalAltResult,
> {
match lhs {
// id[idx_expr]
Expr::Variable(id, _) => Self::search_scope(
scope,
&id,
|val| self.get_indexed_value(&val, idx, idx_expr.position(), op_pos),
lhs.position(),
)
.map(|(src, (val, src_type))| {
(
src_type,
Expr::Variable(id, _) => {
let (
ScopeSource {
typ: src_type,
index: src_idx,
..
},
val,
) = Self::search_scope(scope, &id, lhs.position())?;
let (val, idx_src_type, idx) =
self.get_indexed_value(scope, &val, idx_expr, op_pos, level)?;
Ok((
idx_src_type,
Some(ScopeSource {
name: &id,
typ: src.typ,
index: src.index,
typ: src_type,
index: src_idx,
}),
idx as usize,
idx,
val,
)
}),
))
}
// (expr)[idx_expr]
expr => {
let val = self.eval_expr(scope, expr, level)?;
self.get_indexed_value(&val, idx, idx_expr.position(), op_pos)
.map(|(v, _)| (IndexSourceType::Expression, None, idx as usize, v))
self.get_indexed_value(scope, &val, idx_expr, op_pos, level)
.map(|(v, _, idx)| (IndexSourceType::Expression, None, idx, v))
}
}
}
@ -685,17 +741,25 @@ impl Engine<'_> {
/// Update the value at an index position in a variable inside the scope
#[cfg(not(feature = "no_index"))]
fn update_indexed_var_in_scope(
src_type: IndexSourceType,
idx_src_type: IndexSourceType,
scope: &mut Scope,
src: ScopeSource,
idx: usize,
idx: (Option<usize>, Option<String>),
new_val: (Dynamic, Position),
) -> Result<Dynamic, EvalAltResult> {
match src_type {
match idx_src_type {
// array_id[idx] = val
IndexSourceType::Array => {
let arr = scope.get_mut_by_type::<Array>(src);
arr[idx as usize] = new_val.0;
arr[idx.0.expect("should be Some")] = new_val.0;
Ok(().into_dynamic())
}
// map_id[idx] = val
#[cfg(not(feature = "no_object"))]
IndexSourceType::Map => {
let arr = scope.get_mut_by_type::<Map>(src);
arr.insert(idx.1.expect("should be Some"), new_val.0);
Ok(().into_dynamic())
}
@ -708,7 +772,7 @@ impl Engine<'_> {
.0
.downcast::<char>()
.map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?;
Self::str_replace_char(s, idx as usize, ch);
Self::str_replace_char(s, idx.0.expect("should be Some"), ch);
Ok(().into_dynamic())
}
@ -720,26 +784,37 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))]
fn update_indexed_value(
mut target: Dynamic,
idx: usize,
idx: (Option<usize>, Option<String>),
new_val: Dynamic,
pos: Position,
) -> Result<Dynamic, EvalAltResult> {
if target.is::<Array>() {
let arr = target.downcast_mut::<Array>().expect("array expected");
arr[idx as usize] = new_val;
} else if target.is::<String>() {
arr[idx.0.expect("should be Some")] = new_val;
return Ok(target);
}
#[cfg(not(feature = "no_object"))]
{
if target.is::<Map>() {
let map = target.downcast_mut::<Map>().expect("array expected");
map.insert(idx.1.expect("should be Some"), new_val);
return Ok(target);
}
}
if target.is::<String>() {
let s = target.downcast_mut::<String>().expect("string expected");
// Value must be a character
let ch = *new_val
.downcast::<char>()
.map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?;
Self::str_replace_char(s, idx as usize, ch);
} else {
// All other variable types should be an error
panic!("array or string source type expected for indexing")
Self::str_replace_char(s, idx.0.expect("should be Some"), ch);
return Ok(target);
}
Ok(target)
// All other variable types should be an error
panic!("array, map or string source type expected for indexing")
}
/// Chain-evaluate a dot setter
@ -770,13 +845,10 @@ impl Engine<'_> {
self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0)
.and_then(|v| {
let idx = self.eval_index_value(scope, idx_expr, level)?;
Self::update_indexed_value(
v,
idx as usize,
new_val.0.clone(),
new_val.1,
)
let (_, _, idx) =
self.get_indexed_value(scope, &v, idx_expr, *op_pos, level)?;
Self::update_indexed_value(v, idx, new_val.0.clone(), new_val.1)
})
.and_then(|mut v| {
let set_fn_name = format!("{}{}", FUNC_SETTER, id);
@ -820,16 +892,15 @@ impl Engine<'_> {
self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0)
.and_then(|v| {
let idx = self.eval_index_value(scope, idx_expr, level)?;
let (mut target, _) =
self.get_indexed_value(&v, idx, idx_expr.position(), *op_pos)?;
let (mut target, _, idx) =
self.get_indexed_value(scope, &v, idx_expr, *op_pos, level)?;
let val_pos = new_val.1;
let this_ptr = target.as_mut();
self.set_dot_val_helper(scope, this_ptr, rhs, new_val, level)?;
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
Self::update_indexed_value(v, idx as usize, target, val_pos)
Self::update_indexed_value(v, idx, target, val_pos)
})
.and_then(|mut v| {
let set_fn_name = format!("{}{}", FUNC_SETTER, id);
@ -874,7 +945,7 @@ impl Engine<'_> {
match dot_lhs {
// id.???
Expr::Variable(id, pos) => {
let (entry, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
let (entry, mut target) = Self::search_scope(scope, id, *pos)?;
match entry.typ {
ScopeEntryType::Constant => Err(EvalAltResult::ErrorAssignmentToConstant(
@ -899,7 +970,7 @@ impl Engine<'_> {
// TODO - Allow chaining of indexing!
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, op_pos) => {
let (src_type, src, idx, mut target) =
let (idx_src_type, src, idx, mut target) =
self.eval_index_expr(scope, lhs, idx_expr, *op_pos, level)?;
let val_pos = new_val.1;
let this_ptr = target.as_mut();
@ -916,7 +987,7 @@ impl Engine<'_> {
}
ScopeEntryType::Normal => {
Self::update_indexed_var_in_scope(
src_type,
idx_src_type,
scope,
src,
idx,
@ -951,7 +1022,7 @@ impl Engine<'_> {
Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()),
Expr::StringConstant(s, _) => Ok(s.into_dynamic()),
Expr::CharConstant(c, _) => Ok(c.into_dynamic()),
Expr::Variable(id, pos) => Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val),
Expr::Variable(id, pos) => Self::search_scope(scope, id, *pos).map(|(_, val)| val),
Expr::Property(_, _) => panic!("unexpected property."),
// lhs[idx_expr]
@ -998,7 +1069,7 @@ impl Engine<'_> {
// idx_lhs[idx_expr] = rhs
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, idx_expr, op_pos) => {
let (src_type, src, idx, _) =
let (idx_src_type, src, idx, _) =
self.eval_index_expr(scope, idx_lhs, idx_expr, *op_pos, level)?;
if let Some(src) = src {
@ -1010,7 +1081,7 @@ impl Engine<'_> {
))
}
ScopeEntryType::Normal => Ok(Self::update_indexed_var_in_scope(
src_type,
idx_src_type,
scope,
src,
idx,
@ -1051,7 +1122,7 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))]
Expr::Array(contents, _) => {
let mut arr = Vec::new();
let mut arr = Array::new();
contents.into_iter().try_for_each(|item| {
self.eval_expr(scope, item, level).map(|val| arr.push(val))
@ -1060,6 +1131,19 @@ impl Engine<'_> {
Ok(Box::new(arr))
}
#[cfg(not(feature = "no_object"))]
Expr::Map(contents, _) => {
let mut map = Map::new();
contents.into_iter().try_for_each(|item| {
self.eval_expr(scope, &item.1, level).map(|val| {
map.insert(item.0.clone(), val);
})
})?;
Ok(Box::new(map))
}
Expr::FunctionCall(fn_name, args_expr_list, def_val, pos) => {
// Has a system function an override?
fn has_override(engine: &Engine, name: &str) -> bool {

View File

@ -54,8 +54,13 @@ pub enum ParseErrorType {
/// An expression in indexing brackets `[]` has syntax error.
#[cfg(not(feature = "no_index"))]
MalformedIndexExpr(String),
/// A map definition has duplicated property names. Wrapped is the property name.
#[cfg(not(feature = "no_object"))]
DuplicatedProperty(String),
/// Invalid expression assigned to constant.
ForbiddenConstantExpr(String),
/// Missing a property name for maps.
PropertyExpected,
/// Missing a variable name after the `let`, `const` or `for` keywords.
VariableExpected,
/// Missing an expression.
@ -121,7 +126,10 @@ impl ParseError {
ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments",
#[cfg(not(feature = "no_index"))]
ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression",
#[cfg(not(feature = "no_object"))]
ParseErrorType::DuplicatedProperty(_) => "Duplicated property in object map literal",
ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant",
ParseErrorType::PropertyExpected => "Expecting name of a property",
ParseErrorType::VariableExpected => "Expecting name of a variable",
ParseErrorType::ExprExpected(_) => "Expecting an expression",
#[cfg(not(feature = "no_function"))]
@ -160,6 +168,11 @@ impl fmt::Display for ParseError {
write!(f, "{}", if s.is_empty() { self.desc() } else { s })?
}
#[cfg(not(feature = "no_object"))]
ParseErrorType::DuplicatedProperty(ref s) => {
write!(f, "Duplicated property '{}' for object map literal", s)?
}
ParseErrorType::ExprExpected(ref s) => write!(f, "Expecting {} expression", s)?,
#[cfg(not(feature = "no_function"))]

View File

@ -68,6 +68,9 @@ pub use scope::Scope;
#[cfg(not(feature = "no_index"))]
pub use engine::Array;
#[cfg(not(feature = "no_object"))]
pub use engine::Map;
#[cfg(not(feature = "no_float"))]
pub use parser::FLOAT;

View File

@ -371,19 +371,23 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr {
// [ items .. ]
#[cfg(not(feature = "no_index"))]
Expr::Array(items, pos) => {
let orig_len = items.len();
let items: Vec<_> = items
.into_iter()
.map(|expr| optimize_expr(expr, state))
.collect();
if orig_len != items.len() {
state.set_dirty();
}
Expr::Array(items, pos)
}
// [ items .. ]
#[cfg(not(feature = "no_object"))]
Expr::Map(items, pos) => {
let items: Vec<_> = items
.into_iter()
.map(|(key, expr, pos)| (key, optimize_expr(expr, state), pos))
.collect();
Expr::Map(items, pos)
}
// lhs && rhs
Expr::And(lhs, rhs) => match (*lhs, *rhs) {
// true && rhs -> rhs

View File

@ -11,7 +11,9 @@ use crate::optimize::optimize_into_ast;
use crate::stdlib::{
borrow::Cow,
boxed::Box,
char, fmt, format,
char,
collections::HashMap,
fmt, format,
iter::Peekable,
ops::Add,
str::Chars,
@ -365,6 +367,9 @@ pub enum Expr {
#[cfg(not(feature = "no_index"))]
/// [ expr, ... ]
Array(Vec<Expr>, Position),
#[cfg(not(feature = "no_object"))]
/// ${ name:expr, ... }
Map(Vec<(String, Expr, Position)>, Position),
/// lhs && rhs
And(Box<Expr>, Box<Expr>),
/// lhs || rhs
@ -399,6 +404,13 @@ impl Expr {
.collect::<Vec<_>>()
.into_dynamic(),
#[cfg(not(feature = "no_object"))]
Expr::Map(items, _) if items.iter().all(|(_, v, _)| v.is_constant()) => items
.iter()
.map(|(k, v, _)| (k.clone(), v.get_constant_value()))
.collect::<HashMap<_, _>>()
.into_dynamic(),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.into_dynamic(),
@ -457,6 +469,9 @@ impl Expr {
#[cfg(not(feature = "no_index"))]
Expr::Array(_, pos) => *pos,
#[cfg(not(feature = "no_object"))]
Expr::Map(_, pos) => *pos,
#[cfg(not(feature = "no_index"))]
Expr::Index(expr, _, _) => expr.position(),
}
@ -534,6 +549,8 @@ pub enum Token {
Colon,
Comma,
Period,
#[cfg(not(feature = "no_object"))]
MapStart,
Equals,
True,
False,
@ -609,6 +626,8 @@ impl Token {
Colon => ":",
Comma => ",",
Period => ".",
#[cfg(not(feature = "no_object"))]
MapStart => "${",
Equals => "=",
True => "true",
False => "false",
@ -797,6 +816,11 @@ pub struct TokenIterator<'a> {
}
impl<'a> TokenIterator<'a> {
/// Consume the next character.
fn eat_next(&mut self) {
self.stream.next();
self.advance();
}
/// Move the current position one character ahead.
fn advance(&mut self) {
self.pos.advance();
@ -936,20 +960,17 @@ impl<'a> TokenIterator<'a> {
match next_char {
'0'..='9' | '_' => {
result.push(next_char);
self.stream.next();
self.advance();
self.eat_next();
}
#[cfg(not(feature = "no_float"))]
'.' => {
result.push(next_char);
self.stream.next();
self.advance();
self.eat_next();
while let Some(&next_char_in_float) = self.stream.peek() {
match next_char_in_float {
'0'..='9' | '_' => {
result.push(next_char_in_float);
self.stream.next();
self.advance();
self.eat_next();
}
_ => break,
}
@ -960,8 +981,7 @@ impl<'a> TokenIterator<'a> {
if c == '0' =>
{
result.push(next_char);
self.stream.next();
self.advance();
self.eat_next();
let valid = match ch {
'x' | 'X' => [
@ -992,8 +1012,7 @@ impl<'a> TokenIterator<'a> {
}
result.push(next_char_in_hex);
self.stream.next();
self.advance();
self.eat_next();
}
}
@ -1047,8 +1066,7 @@ impl<'a> TokenIterator<'a> {
match next_char {
x if x.is_ascii_alphanumeric() || x == '_' => {
result.push(x);
self.stream.next();
self.advance();
self.eat_next();
}
_ => break,
}
@ -1139,10 +1157,16 @@ impl<'a> TokenIterator<'a> {
#[cfg(not(feature = "no_index"))]
(']', _) => return Some((Token::RightBracket, pos)),
// Map literal
#[cfg(not(feature = "no_object"))]
('$', '{') => {
self.eat_next();
return Some((Token::MapStart, pos));
}
// Operators
('+', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::PlusAssign, pos));
}
('+', _) if self.can_be_unary => return Some((Token::UnaryPlus, pos)),
@ -1151,24 +1175,21 @@ impl<'a> TokenIterator<'a> {
('-', '0'..='9') if self.can_be_unary => negated = true,
('-', '0'..='9') => return Some((Token::Minus, pos)),
('-', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::MinusAssign, pos));
}
('-', _) if self.can_be_unary => return Some((Token::UnaryMinus, pos)),
('-', _) => return Some((Token::Minus, pos)),
('*', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::MultiplyAssign, pos));
}
('*', _) => return Some((Token::Multiply, pos)),
// Comments
('/', '/') => {
self.stream.next();
self.advance();
self.eat_next();
while let Some(c) = self.stream.next() {
if c == '\n' {
@ -1182,8 +1203,7 @@ impl<'a> TokenIterator<'a> {
('/', '*') => {
let mut level = 1;
self.stream.next();
self.advance();
self.eat_next();
while let Some(c) = self.stream.next() {
self.advance();
@ -1212,8 +1232,7 @@ impl<'a> TokenIterator<'a> {
}
('/', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::DivideAssign, pos));
}
('/', _) => return Some((Token::Divide, pos)),
@ -1224,25 +1243,21 @@ impl<'a> TokenIterator<'a> {
('.', _) => return Some((Token::Period, pos)),
('=', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::EqualsTo, pos));
}
('=', _) => return Some((Token::Equals, pos)),
('<', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::LessThanEqualsTo, pos));
}
('<', '<') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((
if self.stream.peek() == Some(&'=') {
self.stream.next();
self.advance();
self.eat_next();
Token::LeftShiftAssign
} else {
Token::LeftShift
@ -1253,18 +1268,15 @@ impl<'a> TokenIterator<'a> {
('<', _) => return Some((Token::LessThan, pos)),
('>', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::GreaterThanEqualsTo, pos));
}
('>', '>') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((
if self.stream.peek() == Some(&'=') {
self.stream.next();
self.advance();
self.eat_next();
Token::RightShiftAssign
} else {
Token::RightShift
@ -1275,53 +1287,45 @@ impl<'a> TokenIterator<'a> {
('>', _) => return Some((Token::GreaterThan, pos)),
('!', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::NotEqualsTo, pos));
}
('!', _) => return Some((Token::Bang, pos)),
('|', '|') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::Or, pos));
}
('|', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::OrAssign, pos));
}
('|', _) => return Some((Token::Pipe, pos)),
('&', '&') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::And, pos));
}
('&', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::AndAssign, pos));
}
('&', _) => return Some((Token::Ampersand, pos)),
('^', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::XOrAssign, pos));
}
('^', _) => return Some((Token::XOr, pos)),
('%', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::ModuloAssign, pos));
}
('%', _) => return Some((Token::Modulo, pos)),
('~', '=') => {
self.stream.next();
self.advance();
self.eat_next();
return Some((Token::PowerOfAssign, pos));
}
('~', _) => return Some((Token::PowerOf, pos)),
@ -1435,7 +1439,7 @@ fn parse_call_expr<'a>(
}
}
/// Parse an indexing expression.s
/// Parse an indexing expression.
#[cfg(not(feature = "no_index"))]
fn parse_index_expr<'a>(
lhs: Box<Expr>,
@ -1445,7 +1449,7 @@ fn parse_index_expr<'a>(
) -> Result<Expr, ParseError> {
let idx_expr = parse_expr(input, allow_stmt_expr)?;
// Check type of indexing - must be integer
// Check type of indexing - must be integer or string
match &idx_expr {
// lhs[int]
Expr::IntegerConstant(i, pos) if *i < 0 => {
@ -1455,6 +1459,72 @@ fn parse_index_expr<'a>(
))
.into_err(*pos))
}
Expr::IntegerConstant(_, pos) => match *lhs {
Expr::Array(_, _) | Expr::StringConstant(_, _) => (),
#[cfg(not(feature = "no_object"))]
Expr::Map(_, _) => {
return Err(PERR::MalformedIndexExpr(
"Object map access expects string index, not a number".into(),
)
.into_err(*pos))
}
Expr::FloatConstant(_, pos)
| Expr::CharConstant(_, pos)
| Expr::Assignment(_, _, pos)
| Expr::Unit(pos)
| Expr::True(pos)
| Expr::False(pos) => {
return Err(PERR::MalformedIndexExpr(
"Only arrays, object maps and strings can be indexed".into(),
)
.into_err(pos))
}
Expr::And(lhs, _) | Expr::Or(lhs, _) => {
return Err(PERR::MalformedIndexExpr(
"Only arrays, object maps and strings can be indexed".into(),
)
.into_err(lhs.position()))
}
_ => (),
},
// lhs[string]
Expr::StringConstant(_, pos) => match *lhs {
#[cfg(not(feature = "no_object"))]
Expr::Map(_, _) => (),
Expr::Array(_, _) | Expr::StringConstant(_, _) => {
return Err(PERR::MalformedIndexExpr(
"Array or string expects numeric index, not a string".into(),
)
.into_err(*pos))
}
Expr::FloatConstant(_, pos)
| Expr::CharConstant(_, pos)
| Expr::Assignment(_, _, pos)
| Expr::Unit(pos)
| Expr::True(pos)
| Expr::False(pos) => {
return Err(PERR::MalformedIndexExpr(
"Only arrays, object maps and strings can be indexed".into(),
)
.into_err(pos))
}
Expr::And(lhs, _) | Expr::Or(lhs, _) => {
return Err(PERR::MalformedIndexExpr(
"Only arrays, object maps and strings can be indexed".into(),
)
.into_err(lhs.position()))
}
_ => (),
},
// lhs[float]
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, pos) => {
@ -1470,13 +1540,6 @@ fn parse_index_expr<'a>(
)
.into_err(*pos))
}
// lhs[string]
Expr::StringConstant(_, pos) => {
return Err(PERR::MalformedIndexExpr(
"Array access expects integer index, not a string".into(),
)
.into_err(*pos))
}
// lhs[??? = ??? ], lhs[()]
Expr::Assignment(_, _, pos) | Expr::Unit(pos) => {
return Err(PERR::MalformedIndexExpr(
@ -1577,7 +1640,7 @@ fn parse_array_literal<'a>(
(_, pos) => {
return Err(PERR::MissingToken(
",".into(),
"separate the item of this array literal".into(),
"to separate the items of this array literal".into(),
)
.into_err(*pos))
}
@ -1598,6 +1661,110 @@ fn parse_array_literal<'a>(
}
}
/// Parse a map literal.
#[cfg(not(feature = "no_object"))]
fn parse_map_literal<'a>(
input: &mut Peekable<TokenIterator<'a>>,
begin: Position,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let mut map = Vec::new();
if !matches!(input.peek(), Some((Token::RightBrace, _))) {
while input.peek().is_some() {
let (name, pos) = match input.next().ok_or_else(|| {
PERR::MissingToken("}".into(), "to end this object map literal".into())
.into_err_eof()
})? {
(Token::Identifier(s), pos) => (s.clone(), pos),
(_, pos) if map.is_empty() => {
return Err(PERR::MissingToken(
"}".into(),
"to end this object map literal".into(),
)
.into_err(pos))
}
(_, pos) => return Err(PERR::PropertyExpected.into_err(pos)),
};
match input.next().ok_or_else(|| {
PERR::MissingToken(
":".into(),
format!(
"to follow the property '{}' in this object map literal",
name
),
)
.into_err_eof()
})? {
(Token::Colon, _) => (),
(_, pos) => {
return Err(PERR::MissingToken(
":".into(),
format!(
"to follow the property '{}' in this object map literal",
name
),
)
.into_err(pos))
}
};
let expr = parse_expr(input, allow_stmt_expr)?;
map.push((name, expr, pos));
match input.peek().ok_or_else(|| {
PERR::MissingToken("}".into(), "to end this object map literal".into())
.into_err_eof()
})? {
(Token::Comma, _) => {
input.next();
}
(Token::RightBrace, _) => break,
(Token::Identifier(_), pos) => {
return Err(PERR::MissingToken(
",".into(),
"to separate the items of this object map literal".into(),
)
.into_err(*pos))
}
(_, pos) => {
return Err(PERR::MissingToken(
"}".into(),
"to end this object map literal".into(),
)
.into_err(*pos))
}
}
}
}
// Check for duplicating properties
map.iter()
.enumerate()
.try_for_each(|(i, (k1, _, _))| {
map.iter()
.skip(i + 1)
.find(|(k2, _, _)| k2 == k1)
.map_or_else(|| Ok(()), |(k2, _, pos)| Err((k2, *pos)))
})
.map_err(|(key, pos)| PERR::DuplicatedProperty(key.to_string()).into_err(pos))?;
// Ending brace
match input.peek().ok_or_else(|| {
PERR::MissingToken("}".into(), "to end this object map literal".into()).into_err_eof()
})? {
(Token::RightBrace, _) => {
input.next();
Ok(Expr::Map(map, begin))
}
(_, pos) => Err(
PERR::MissingToken("]".into(), "to end this object map literal".into()).into_err(*pos),
),
}
}
/// Parse a primary expression.
fn parse_primary<'a>(
input: &mut Peekable<TokenIterator<'a>>,
@ -1641,6 +1808,11 @@ fn parse_primary<'a>(
can_be_indexed = true;
parse_array_literal(input, pos, allow_stmt_expr)
}
#[cfg(not(feature = "no_object"))]
(Token::MapStart, pos) => {
can_be_indexed = true;
parse_map_literal(input, pos, allow_stmt_expr)
}
(Token::True, pos) => Ok(Expr::True(pos)),
(Token::False, pos) => Ok(Expr::False(pos)),
(Token::LexError(err), pos) => Err(PERR::BadInput(err.to_string()).into_err(pos)),

View File

@ -36,10 +36,12 @@ 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 and not a string.
/// Trying to index into a type that is not an array, an object map, or a string.
ErrorIndexingType(String, Position),
/// Trying to index into an array or string with an index that is not `i64`.
ErrorIndexExpr(Position),
ErrorNumericIndexExpr(Position),
/// Trying to index into a map with an index that is not `String`.
ErrorStringIndexExpr(Position),
/// The guard expression in an `if` or `while` statement does not return a boolean value.
ErrorLogicGuard(Position),
/// The `for` statement encounters a type that is not an iterator.
@ -81,9 +83,12 @@ impl EvalAltResult {
}
Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands",
Self::ErrorCharMismatch(_) => "Character expected",
Self::ErrorIndexExpr(_) => "Indexing into an array or string expects an integer index",
Self::ErrorNumericIndexExpr(_) => {
"Indexing into an array or string expects an integer index"
}
Self::ErrorStringIndexExpr(_) => "Indexing into an object map expects a string index",
Self::ErrorIndexingType(_, _) => {
"Indexing can only be performed on an array or a string"
"Indexing can only be performed on an array, an object map, or a string"
}
Self::ErrorArrayBounds(_, index, _) if *index < 0 => {
"Array access expects non-negative index"
@ -122,22 +127,26 @@ impl fmt::Display for EvalAltResult {
let desc = self.desc();
match self {
Self::ErrorFunctionNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorVariableNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorIndexingType(_, pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorIndexExpr(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorLogicGuard(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorFunctionNotFound(s, pos) | Self::ErrorVariableNotFound(s, pos) => {
write!(f, "{}: '{}' ({})", desc, s, pos)
}
Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos),
Self::ErrorIndexingType(_, pos)
| Self::ErrorNumericIndexExpr(pos)
| Self::ErrorStringIndexExpr(pos)
| Self::ErrorLogicGuard(pos)
| Self::ErrorFor(pos)
| Self::ErrorAssignmentToUnknownLHS(pos)
| Self::ErrorDotExpr(_, pos)
| Self::ErrorStackOverflow(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos),
Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos),
Self::ErrorDotExpr(_, pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorArithmetic(s, pos) => write!(f, "{} ({})", s, pos),
Self::ErrorStackOverflow(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorRuntime(s, pos) => {
write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos)
}
Self::ErrorArithmetic(s, pos) => write!(f, "{} ({})", s, pos),
Self::ErrorLoopBreak(pos) => write!(f, "{} ({})", desc, pos),
Self::Return(_, pos) => write!(f, "{} ({})", desc, pos),
#[cfg(not(feature = "no_std"))]
@ -225,7 +234,8 @@ impl EvalAltResult {
| Self::ErrorArrayBounds(_, _, pos)
| Self::ErrorStringBounds(_, _, pos)
| Self::ErrorIndexingType(_, pos)
| Self::ErrorIndexExpr(pos)
| Self::ErrorNumericIndexExpr(pos)
| Self::ErrorStringIndexExpr(pos)
| Self::ErrorLogicGuard(pos)
| Self::ErrorFor(pos)
| Self::ErrorVariableNotFound(_, pos)
@ -256,7 +266,8 @@ impl EvalAltResult {
| Self::ErrorArrayBounds(_, _, ref mut pos)
| Self::ErrorStringBounds(_, _, ref mut pos)
| Self::ErrorIndexingType(_, ref mut pos)
| Self::ErrorIndexExpr(ref mut pos)
| Self::ErrorNumericIndexExpr(ref mut pos)
| Self::ErrorStringIndexExpr(ref mut pos)
| Self::ErrorLogicGuard(ref mut pos)
| Self::ErrorFor(ref mut pos)
| Self::ErrorVariableNotFound(_, ref mut pos)

63
tests/maps.rs Normal file
View File

@ -0,0 +1,63 @@
#![cfg(not(feature = "no_object"))]
#![cfg(not(feature = "no_index"))]
use rhai::{AnyExt, Dynamic, Engine, EvalAltResult, Map, RegisterFn, INT};
#[test]
fn test_map_indexing() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(
engine.eval::<INT>(r#"let x = ${a: 1, b: 2, c: 3}; x["b"]"#)?,
2
);
assert_eq!(
engine.eval::<INT>("let y = ${a: 1, b: 2, c: 3}; y.a = 5; y.a")?,
5
);
assert_eq!(
engine.eval::<char>(
r#"
let y = ${d: 1, e: ${a: 42, b: 88, c: "93"}, x: 9};
y.e["c"][1]
"#
)?,
'3'
);
engine.eval::<()>("let y = ${a: 1, b: 2, c: 3}; y.z")?;
Ok(())
}
#[test]
fn test_map_assign() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let mut x = engine.eval::<Map>("let x = ${a: 1, b: true, c: \"3\"}; x")?;
let box_a = x.remove("a").unwrap();
let box_b = x.remove("b").unwrap();
let box_c = x.remove("c").unwrap();
assert_eq!(*box_a.downcast::<INT>().unwrap(), 1);
assert_eq!(*box_b.downcast::<bool>().unwrap(), true);
assert_eq!(*box_c.downcast::<String>().unwrap(), "3");
Ok(())
}
#[test]
fn test_map_return() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let mut x = engine.eval::<Map>("${a: 1, b: true, c: \"3\"}")?;
let box_a = x.remove("a").unwrap();
let box_b = x.remove("b").unwrap();
let box_c = x.remove("c").unwrap();
assert_eq!(*box_a.downcast::<INT>().unwrap(), 1);
assert_eq!(*box_b.downcast::<bool>().unwrap(), true);
assert_eq!(*box_c.downcast::<String>().unwrap(), "3".to_string());
Ok(())
}