Add object maps.
This commit is contained in:
parent
ef6c6ea6d2
commit
45ee51874f
65
README.md
65
README.md
@ -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
|
||||
--------------------
|
||||
|
||||
|
@ -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)
|
||||
|
306
src/engine.rs
306
src/engine.rs
@ -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 {
|
||||
|
13
src/error.rs
13
src/error.rs
@ -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"))]
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
302
src/parser.rs
302
src/parser.rs
@ -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)),
|
||||
|
@ -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
63
tests/maps.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user