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'