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 character** | `char` | `"char"` |
| **Unicode string** | `String` (_not_ `&str`) | `"string"` | | **Unicode string** | `String` (_not_ `&str`) | `"string"` |
| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | | **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_ | | **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_ | | **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) | `()` | `"()"` | | **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` |
@ -999,7 +1000,7 @@ let foo = [1, 2, 3][0];
foo == 1; foo == 1;
fn abc() { fn abc() {
[42, 43, 44] // a function returning an array literal [42, 43, 44] // a function returning an array
} }
let foo = abc()[0]; 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)) ); 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 Comparison operators
-------------------- --------------------

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,9 @@ use crate::optimize::optimize_into_ast;
use crate::stdlib::{ use crate::stdlib::{
borrow::Cow, borrow::Cow,
boxed::Box, boxed::Box,
char, fmt, format, char,
collections::HashMap,
fmt, format,
iter::Peekable, iter::Peekable,
ops::Add, ops::Add,
str::Chars, str::Chars,
@ -365,6 +367,9 @@ pub enum Expr {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
/// [ expr, ... ] /// [ expr, ... ]
Array(Vec<Expr>, Position), Array(Vec<Expr>, Position),
#[cfg(not(feature = "no_object"))]
/// ${ name:expr, ... }
Map(Vec<(String, Expr, Position)>, Position),
/// lhs && rhs /// lhs && rhs
And(Box<Expr>, Box<Expr>), And(Box<Expr>, Box<Expr>),
/// lhs || rhs /// lhs || rhs
@ -399,6 +404,13 @@ impl Expr {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_dynamic(), .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"))] #[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.into_dynamic(), Expr::FloatConstant(f, _) => f.into_dynamic(),
@ -457,6 +469,9 @@ impl Expr {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Array(_, pos) => *pos, Expr::Array(_, pos) => *pos,
#[cfg(not(feature = "no_object"))]
Expr::Map(_, pos) => *pos,
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(expr, _, _) => expr.position(), Expr::Index(expr, _, _) => expr.position(),
} }
@ -534,6 +549,8 @@ pub enum Token {
Colon, Colon,
Comma, Comma,
Period, Period,
#[cfg(not(feature = "no_object"))]
MapStart,
Equals, Equals,
True, True,
False, False,
@ -609,6 +626,8 @@ impl Token {
Colon => ":", Colon => ":",
Comma => ",", Comma => ",",
Period => ".", Period => ".",
#[cfg(not(feature = "no_object"))]
MapStart => "${",
Equals => "=", Equals => "=",
True => "true", True => "true",
False => "false", False => "false",
@ -797,6 +816,11 @@ pub struct TokenIterator<'a> {
} }
impl<'a> 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. /// Move the current position one character ahead.
fn advance(&mut self) { fn advance(&mut self) {
self.pos.advance(); self.pos.advance();
@ -936,20 +960,17 @@ impl<'a> TokenIterator<'a> {
match next_char { match next_char {
'0'..='9' | '_' => { '0'..='9' | '_' => {
result.push(next_char); result.push(next_char);
self.stream.next(); self.eat_next();
self.advance();
} }
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
'.' => { '.' => {
result.push(next_char); result.push(next_char);
self.stream.next(); self.eat_next();
self.advance();
while let Some(&next_char_in_float) = self.stream.peek() { while let Some(&next_char_in_float) = self.stream.peek() {
match next_char_in_float { match next_char_in_float {
'0'..='9' | '_' => { '0'..='9' | '_' => {
result.push(next_char_in_float); result.push(next_char_in_float);
self.stream.next(); self.eat_next();
self.advance();
} }
_ => break, _ => break,
} }
@ -960,8 +981,7 @@ impl<'a> TokenIterator<'a> {
if c == '0' => if c == '0' =>
{ {
result.push(next_char); result.push(next_char);
self.stream.next(); self.eat_next();
self.advance();
let valid = match ch { let valid = match ch {
'x' | 'X' => [ 'x' | 'X' => [
@ -992,8 +1012,7 @@ impl<'a> TokenIterator<'a> {
} }
result.push(next_char_in_hex); result.push(next_char_in_hex);
self.stream.next(); self.eat_next();
self.advance();
} }
} }
@ -1047,8 +1066,7 @@ impl<'a> TokenIterator<'a> {
match next_char { match next_char {
x if x.is_ascii_alphanumeric() || x == '_' => { x if x.is_ascii_alphanumeric() || x == '_' => {
result.push(x); result.push(x);
self.stream.next(); self.eat_next();
self.advance();
} }
_ => break, _ => break,
} }
@ -1139,10 +1157,16 @@ impl<'a> TokenIterator<'a> {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
(']', _) => return Some((Token::RightBracket, pos)), (']', _) => return Some((Token::RightBracket, pos)),
// Map literal
#[cfg(not(feature = "no_object"))]
('$', '{') => {
self.eat_next();
return Some((Token::MapStart, pos));
}
// Operators // Operators
('+', '=') => { ('+', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::PlusAssign, pos)); return Some((Token::PlusAssign, pos));
} }
('+', _) if self.can_be_unary => return Some((Token::UnaryPlus, 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') if self.can_be_unary => negated = true,
('-', '0'..='9') => return Some((Token::Minus, pos)), ('-', '0'..='9') => return Some((Token::Minus, pos)),
('-', '=') => { ('-', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::MinusAssign, pos)); return Some((Token::MinusAssign, pos));
} }
('-', _) if self.can_be_unary => return Some((Token::UnaryMinus, pos)), ('-', _) if self.can_be_unary => return Some((Token::UnaryMinus, pos)),
('-', _) => return Some((Token::Minus, pos)), ('-', _) => return Some((Token::Minus, pos)),
('*', '=') => { ('*', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::MultiplyAssign, pos)); return Some((Token::MultiplyAssign, pos));
} }
('*', _) => return Some((Token::Multiply, pos)), ('*', _) => return Some((Token::Multiply, pos)),
// Comments // Comments
('/', '/') => { ('/', '/') => {
self.stream.next(); self.eat_next();
self.advance();
while let Some(c) = self.stream.next() { while let Some(c) = self.stream.next() {
if c == '\n' { if c == '\n' {
@ -1182,8 +1203,7 @@ impl<'a> TokenIterator<'a> {
('/', '*') => { ('/', '*') => {
let mut level = 1; let mut level = 1;
self.stream.next(); self.eat_next();
self.advance();
while let Some(c) = self.stream.next() { while let Some(c) = self.stream.next() {
self.advance(); self.advance();
@ -1212,8 +1232,7 @@ impl<'a> TokenIterator<'a> {
} }
('/', '=') => { ('/', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::DivideAssign, pos)); return Some((Token::DivideAssign, pos));
} }
('/', _) => return Some((Token::Divide, pos)), ('/', _) => return Some((Token::Divide, pos)),
@ -1224,25 +1243,21 @@ impl<'a> TokenIterator<'a> {
('.', _) => return Some((Token::Period, pos)), ('.', _) => return Some((Token::Period, pos)),
('=', '=') => { ('=', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::EqualsTo, pos)); return Some((Token::EqualsTo, pos));
} }
('=', _) => return Some((Token::Equals, pos)), ('=', _) => return Some((Token::Equals, pos)),
('<', '=') => { ('<', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::LessThanEqualsTo, pos)); return Some((Token::LessThanEqualsTo, pos));
} }
('<', '<') => { ('<', '<') => {
self.stream.next(); self.eat_next();
self.advance();
return Some(( return Some((
if self.stream.peek() == Some(&'=') { if self.stream.peek() == Some(&'=') {
self.stream.next(); self.eat_next();
self.advance();
Token::LeftShiftAssign Token::LeftShiftAssign
} else { } else {
Token::LeftShift Token::LeftShift
@ -1253,18 +1268,15 @@ impl<'a> TokenIterator<'a> {
('<', _) => return Some((Token::LessThan, pos)), ('<', _) => return Some((Token::LessThan, pos)),
('>', '=') => { ('>', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::GreaterThanEqualsTo, pos)); return Some((Token::GreaterThanEqualsTo, pos));
} }
('>', '>') => { ('>', '>') => {
self.stream.next(); self.eat_next();
self.advance();
return Some(( return Some((
if self.stream.peek() == Some(&'=') { if self.stream.peek() == Some(&'=') {
self.stream.next(); self.eat_next();
self.advance();
Token::RightShiftAssign Token::RightShiftAssign
} else { } else {
Token::RightShift Token::RightShift
@ -1275,53 +1287,45 @@ impl<'a> TokenIterator<'a> {
('>', _) => return Some((Token::GreaterThan, pos)), ('>', _) => return Some((Token::GreaterThan, pos)),
('!', '=') => { ('!', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::NotEqualsTo, pos)); return Some((Token::NotEqualsTo, pos));
} }
('!', _) => return Some((Token::Bang, pos)), ('!', _) => return Some((Token::Bang, pos)),
('|', '|') => { ('|', '|') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::Or, pos)); return Some((Token::Or, pos));
} }
('|', '=') => { ('|', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::OrAssign, pos)); return Some((Token::OrAssign, pos));
} }
('|', _) => return Some((Token::Pipe, pos)), ('|', _) => return Some((Token::Pipe, pos)),
('&', '&') => { ('&', '&') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::And, pos)); return Some((Token::And, pos));
} }
('&', '=') => { ('&', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::AndAssign, pos)); return Some((Token::AndAssign, pos));
} }
('&', _) => return Some((Token::Ampersand, pos)), ('&', _) => return Some((Token::Ampersand, pos)),
('^', '=') => { ('^', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::XOrAssign, pos)); return Some((Token::XOrAssign, pos));
} }
('^', _) => return Some((Token::XOr, pos)), ('^', _) => return Some((Token::XOr, pos)),
('%', '=') => { ('%', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::ModuloAssign, pos)); return Some((Token::ModuloAssign, pos));
} }
('%', _) => return Some((Token::Modulo, pos)), ('%', _) => return Some((Token::Modulo, pos)),
('~', '=') => { ('~', '=') => {
self.stream.next(); self.eat_next();
self.advance();
return Some((Token::PowerOfAssign, pos)); return Some((Token::PowerOfAssign, pos));
} }
('~', _) => return Some((Token::PowerOf, 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"))] #[cfg(not(feature = "no_index"))]
fn parse_index_expr<'a>( fn parse_index_expr<'a>(
lhs: Box<Expr>, lhs: Box<Expr>,
@ -1445,7 +1449,7 @@ fn parse_index_expr<'a>(
) -> Result<Expr, ParseError> { ) -> Result<Expr, ParseError> {
let idx_expr = parse_expr(input, allow_stmt_expr)?; 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 { match &idx_expr {
// lhs[int] // lhs[int]
Expr::IntegerConstant(i, pos) if *i < 0 => { Expr::IntegerConstant(i, pos) if *i < 0 => {
@ -1455,6 +1459,72 @@ fn parse_index_expr<'a>(
)) ))
.into_err(*pos)) .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] // lhs[float]
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, pos) => { Expr::FloatConstant(_, pos) => {
@ -1470,13 +1540,6 @@ fn parse_index_expr<'a>(
) )
.into_err(*pos)) .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[()] // lhs[??? = ??? ], lhs[()]
Expr::Assignment(_, _, pos) | Expr::Unit(pos) => { Expr::Assignment(_, _, pos) | Expr::Unit(pos) => {
return Err(PERR::MalformedIndexExpr( return Err(PERR::MalformedIndexExpr(
@ -1577,7 +1640,7 @@ fn parse_array_literal<'a>(
(_, pos) => { (_, pos) => {
return Err(PERR::MissingToken( return Err(PERR::MissingToken(
",".into(), ",".into(),
"separate the item of this array literal".into(), "to separate the items of this array literal".into(),
) )
.into_err(*pos)) .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. /// Parse a primary expression.
fn parse_primary<'a>( fn parse_primary<'a>(
input: &mut Peekable<TokenIterator<'a>>, input: &mut Peekable<TokenIterator<'a>>,
@ -1641,6 +1808,11 @@ fn parse_primary<'a>(
can_be_indexed = true; can_be_indexed = true;
parse_array_literal(input, pos, allow_stmt_expr) 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::True, pos) => Ok(Expr::True(pos)),
(Token::False, pos) => Ok(Expr::False(pos)), (Token::False, pos) => Ok(Expr::False(pos)),
(Token::LexError(err), pos) => Err(PERR::BadInput(err.to_string()).into_err(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. /// 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 and not a string. /// Trying to index into a type that is not an array, an object map, or a string.
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`.
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. /// The guard expression in an `if` or `while` statement does not return a boolean value.
ErrorLogicGuard(Position), ErrorLogicGuard(Position),
/// The `for` statement encounters a type that is not an iterator. /// 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::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands",
Self::ErrorCharMismatch(_) => "Character expected", 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(_, _) => { 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 => { Self::ErrorArrayBounds(_, index, _) if *index < 0 => {
"Array access expects non-negative index" "Array access expects non-negative index"
@ -122,22 +127,26 @@ impl fmt::Display for EvalAltResult {
let desc = self.desc(); let desc = self.desc();
match self { match self {
Self::ErrorFunctionNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), Self::ErrorFunctionNotFound(s, pos) | Self::ErrorVariableNotFound(s, pos) => {
Self::ErrorVariableNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), write!(f, "{}: '{}' ({})", desc, s, pos)
Self::ErrorIndexingType(_, pos) => write!(f, "{} ({})", desc, pos), }
Self::ErrorIndexExpr(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos),
Self::ErrorLogicGuard(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorIndexingType(_, pos)
Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, 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::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorMismatchOutputType(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) => { Self::ErrorRuntime(s, pos) => {
write!(f, "{} ({})", if s.is_empty() { desc } else { 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::ErrorLoopBreak(pos) => write!(f, "{} ({})", desc, pos),
Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), Self::Return(_, pos) => write!(f, "{} ({})", desc, pos),
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
@ -225,7 +234,8 @@ impl EvalAltResult {
| Self::ErrorArrayBounds(_, _, pos) | Self::ErrorArrayBounds(_, _, pos)
| Self::ErrorStringBounds(_, _, pos) | Self::ErrorStringBounds(_, _, pos)
| Self::ErrorIndexingType(_, pos) | Self::ErrorIndexingType(_, pos)
| Self::ErrorIndexExpr(pos) | Self::ErrorNumericIndexExpr(pos)
| Self::ErrorStringIndexExpr(pos)
| Self::ErrorLogicGuard(pos) | Self::ErrorLogicGuard(pos)
| Self::ErrorFor(pos) | Self::ErrorFor(pos)
| Self::ErrorVariableNotFound(_, pos) | Self::ErrorVariableNotFound(_, pos)
@ -256,7 +266,8 @@ impl EvalAltResult {
| Self::ErrorArrayBounds(_, _, ref mut pos) | Self::ErrorArrayBounds(_, _, ref mut pos)
| Self::ErrorStringBounds(_, _, ref mut pos) | Self::ErrorStringBounds(_, _, ref mut pos)
| Self::ErrorIndexingType(_, 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::ErrorLogicGuard(ref mut pos)
| Self::ErrorFor(ref mut pos) | Self::ErrorFor(ref mut pos)
| Self::ErrorVariableNotFound(_, 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(())
}