From 45ee51874f427f2d4c07d5c6cb714242f91f4f9c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 29 Mar 2020 23:53:35 +0800 Subject: [PATCH 1/2] Add object maps. --- README.md | 65 +++++++++- src/builtin.rs | 21 +++- src/engine.rs | 306 ++++++++++++++++++++++++++++++------------------ src/error.rs | 13 ++ src/lib.rs | 3 + src/optimize.rs | 16 ++- src/parser.rs | 302 +++++++++++++++++++++++++++++++++++++---------- src/result.rs | 45 ++++--- tests/maps.rs | 63 ++++++++++ 9 files changed, 632 insertions(+), 202 deletions(-) create mode 100644 tests/maps.rs diff --git a/README.md b/README.md index e99847c0..2f36da6f 100644 --- a/README.md +++ b/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`),
`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 -------------------- diff --git a/src/builtin.rs b/src/builtin.rs index 96aab8c2..4f297572 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -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::(|a| { Box::new(a.downcast_ref::().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(x: T, y: String) -> String { format!("{}{}", x, y) diff --git a/src/engine.rs b/src/engine.rs index 2d3ab8a7..c07274b8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -26,6 +26,10 @@ use crate::stdlib::{ #[cfg(not(feature = "no_index"))] pub type Array = Vec; +/// An dynamic hash map of `Dynamic` values. +#[cfg(not(feature = "no_object"))] +pub type Map = HashMap; + pub type FnCallArgs<'a> = [&'a mut Variant]; pub type FnAny = dyn Fn(&mut FnCallArgs, Position) -> Result; @@ -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"), + #[cfg(not(feature = "no_object"))] + (type_name::(), "map"), (type_name::(), "string"), (type_name::(), "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::() { + 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.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, 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 { - self.eval_expr(scope, idx_expr, level)? - .downcast::() - .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, Option)), EvalAltResult> { + let idx_pos = idx_expr.position(); + if val.is::() { // val_array[idx] let arr = val.downcast_ref::().expect("array expected"); - if idx >= 0 { + let idx = *self + .eval_expr(scope, idx_expr, level)? + .downcast::() + .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::() { + // val_map[idx] + let map = val.downcast_ref::().expect("array expected"); + + let idx = *self + .eval_expr(scope, idx_expr, level)? + .downcast::() + .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::() { + } + + if val.is::() { // val_string[idx] let s = val.downcast_ref::().expect("string expected"); - if idx >= 0 { + let idx = *self + .eval_expr(scope, idx_expr, level)? + .downcast::() + .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>, usize, Dynamic), EvalAltResult> { - let idx = self.eval_index_value(scope, idx_expr, level)?; - + ) -> Result< + ( + IndexSourceType, + Option>, + (Option, Option), + 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, Option), new_val: (Dynamic, Position), ) -> Result { - match src_type { + match idx_src_type { // array_id[idx] = val IndexSourceType::Array => { let arr = scope.get_mut_by_type::(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::(src); + arr.insert(idx.1.expect("should be Some"), new_val.0); Ok(().into_dynamic()) } @@ -708,7 +772,7 @@ impl Engine<'_> { .0 .downcast::() .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, Option), new_val: Dynamic, pos: Position, ) -> Result { if target.is::() { let arr = target.downcast_mut::().expect("array expected"); - arr[idx as usize] = new_val; - } else if target.is::() { + arr[idx.0.expect("should be Some")] = new_val; + return Ok(target); + } + + #[cfg(not(feature = "no_object"))] + { + if target.is::() { + let map = target.downcast_mut::().expect("array expected"); + map.insert(idx.1.expect("should be Some"), new_val); + return Ok(target); + } + } + + if target.is::() { let s = target.downcast_mut::().expect("string expected"); // Value must be a character let ch = *new_val .downcast::() .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 { diff --git a/src/error.rs b/src/error.rs index be3af7d0..66f4dc6f 100644 --- a/src/error.rs +++ b/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"))] diff --git a/src/lib.rs b/src/lib.rs index 5d7bc700..a553a3a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/optimize.rs b/src/optimize.rs index 8d0bcc0f..826df90f 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -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 diff --git a/src/parser.rs b/src/parser.rs index f6807eeb..0b6e33e9 100644 --- a/src/parser.rs +++ b/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, Position), + #[cfg(not(feature = "no_object"))] + /// ${ name:expr, ... } + Map(Vec<(String, Expr, Position)>, Position), /// lhs && rhs And(Box, Box), /// lhs || rhs @@ -399,6 +404,13 @@ impl Expr { .collect::>() .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::>() + .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, @@ -1445,7 +1449,7 @@ fn parse_index_expr<'a>( ) -> Result { 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>, + begin: Position, + allow_stmt_expr: bool, +) -> Result { + 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>, @@ -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)), diff --git a/src/result.rs b/src/result.rs index e96a29c7..b0e1ecbd 100644 --- a/src/result.rs +++ b/src/result.rs @@ -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) diff --git a/tests/maps.rs b/tests/maps.rs new file mode 100644 index 00000000..d91d66e6 --- /dev/null +++ b/tests/maps.rs @@ -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::(r#"let x = ${a: 1, b: 2, c: 3}; x["b"]"#)?, + 2 + ); + assert_eq!( + engine.eval::("let y = ${a: 1, b: 2, c: 3}; y.a = 5; y.a")?, + 5 + ); + assert_eq!( + engine.eval::( + 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::("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::().unwrap(), 1); + assert_eq!(*box_b.downcast::().unwrap(), true); + assert_eq!(*box_c.downcast::().unwrap(), "3"); + + Ok(()) +} + +#[test] +fn test_map_return() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + let mut x = engine.eval::("${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::().unwrap(), 1); + assert_eq!(*box_b.downcast::().unwrap(), true); + assert_eq!(*box_c.downcast::().unwrap(), "3".to_string()); + + Ok(()) +} From fce51758d12682ced761b806024e273df03136b3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 30 Mar 2020 12:14:59 +0800 Subject: [PATCH 2/2] Add support for string literal property names in object maps. --- README.md | 28 +++++++++++++++++++++++----- src/error.rs | 2 +- src/parser.rs | 44 ++++++++++++++++++++++++++------------------ tests/maps.rs | 4 ++-- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 2f36da6f..30878dce 100644 --- a/README.md +++ b/README.md @@ -729,6 +729,8 @@ let a = { 40 + 2 }; // 'a' is set to the value of the statement block, which Variables --------- +[variables]: #variables + Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). Variable names must start with an ASCII letter or an underscore '`_`', must contain at least one ASCII letter, and must start with an ASCII letter before a digit. @@ -761,7 +763,7 @@ x == 42; // the parent block's 'x' is not changed Constants --------- -Constants can be defined using the `const` keyword and are immutable. Constants follow the same naming rules as [variables](#variables). +Constants can be defined using the `const` keyword and are immutable. Constants follow the same naming rules as [variables]. ```rust const x = 42; @@ -1046,7 +1048,14 @@ 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 '`,`'. +and separated by commas '`,`'. The property _name_ can be a simple variable name following the same +naming rules as [variables], or an arbitrary string literal. + +Property values can be accessed via the dot notation (_object_ `.` _property_) or index notation (_object_ `[` _property_ `]`). +The dot notation allows only property names that follow the same naming rules as [variables]. +The index notation allows setting/getting properties of arbitrary names (even the empty string). + +**Important:** Trying to read a non-existent property returns `()` instead of causing an error. The Rust type of a Rhai object map is `rhai::Map`. @@ -1068,13 +1077,19 @@ Examples: let y = ${ // object map literal with 3 properties a: 1, bar: "hello", - baz: 123.456 + "baz!$@": 123.456, // like JS, you can use any string as property names... + "": false, // even the empty string! + + a: 42 // <- syntax error: duplicated property name }; -y.a = 42; + +y.a = 42; // access via dot notation +y.baz!$@ = 42; // <- syntax error: only proper variable names allowed in dot notation +y."baz!$@" = 42; // <- syntax error: strings not allowed in dot notation print(y.a); // prints 42 -print(y["bar"]); // prints "hello" - access via string index +print(y["baz!$@"]); // prints 123.456 - access via index notation ts.obj = y; // object maps can be assigned completely (by value copy) let foo = ts.list.a; @@ -1096,6 +1111,9 @@ foo == 42; y.has("a") == true; y.has("xyz") == false; +y.xyz == (); // A non-existing property returns '()' +y["xyz"] == (); + print(y.len()); // prints 3 y.clear(); // empty the object map diff --git a/src/error.rs b/src/error.rs index 66f4dc6f..2a2986b8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,7 +59,7 @@ pub enum ParseErrorType { DuplicatedProperty(String), /// Invalid expression assigned to constant. ForbiddenConstantExpr(String), - /// Missing a property name for maps. + /// Missing a property name for custom types and maps. PropertyExpected, /// Missing a variable name after the `let`, `const` or `for` keywords. VariableExpected, diff --git a/src/parser.rs b/src/parser.rs index 0b6e33e9..64fc5341 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1677,6 +1677,7 @@ fn parse_map_literal<'a>( .into_err_eof() })? { (Token::Identifier(s), pos) => (s.clone(), pos), + (Token::StringConst(s), pos) => (s.clone(), pos), (_, pos) if map.is_empty() => { return Err(PERR::MissingToken( "}".into(), @@ -2053,27 +2054,30 @@ fn parse_binary_op<'a>( #[cfg(not(feature = "no_object"))] Token::Period => { - fn change_var_to_property(expr: Expr) -> Expr { + fn check_property(expr: Expr) -> Result { match expr { - Expr::Dot(lhs, rhs, pos) => Expr::Dot( - Box::new(change_var_to_property(*lhs)), - Box::new(change_var_to_property(*rhs)), + // xxx.lhs.rhs + Expr::Dot(lhs, rhs, pos) => Ok(Expr::Dot( + Box::new(check_property(*lhs)?), + Box::new(check_property(*rhs)?), pos, - ), + )), + // xxx.lhs[idx] #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx, pos) => { - Expr::Index(Box::new(change_var_to_property(*lhs)), idx, pos) + Ok(Expr::Index(Box::new(check_property(*lhs)?), idx, pos)) } - Expr::Variable(s, pos) => Expr::Property(s, pos), - expr => expr, + // xxx.id + Expr::Variable(id, pos) => Ok(Expr::Property(id, pos)), + // xxx.prop + expr @ Expr::Property(_, _) => Ok(expr), + // xxx.fn() + expr @ Expr::FunctionCall(_, _, _, _) => Ok(expr), + expr => Err(PERR::PropertyExpected.into_err(expr.position())), } } - Expr::Dot( - Box::new(current_lhs), - Box::new(change_var_to_property(rhs)), - pos, - ) + Expr::Dot(Box::new(current_lhs), Box::new(check_property(rhs)?), pos) } // Comparison operators default to false when passed invalid operands @@ -2258,12 +2262,16 @@ fn parse_for<'a>( }; // for name in ... - match input - .next() - .ok_or_else(|| PERR::MissingToken("in".into(), "here".into()).into_err_eof())? - { + match input.next().ok_or_else(|| { + PERR::MissingToken("in".into(), "after the iteration variable".into()).into_err_eof() + })? { (Token::In, _) => (), - (_, pos) => return Err(PERR::MissingToken("in".into(), "here".into()).into_err(pos)), + (_, pos) => { + return Err( + PERR::MissingToken("in".into(), "after the iteration variable".into()) + .into_err(pos), + ) + } } // for name in expr { body } diff --git a/tests/maps.rs b/tests/maps.rs index d91d66e6..608c7fad 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -18,8 +18,8 @@ fn test_map_indexing() -> Result<(), EvalAltResult> { assert_eq!( engine.eval::( r#" - let y = ${d: 1, e: ${a: 42, b: 88, c: "93"}, x: 9}; - y.e["c"][1] + let y = ${d: 1, "e": ${a: 42, b: 88, "": "93"}, " 123 xyz": 9}; + y.e[""][1] "# )?, '3'