diff --git a/Cargo.toml b/Cargo.toml index d98dc9ca..063abd0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,13 +20,14 @@ categories = [ "no-std", "embedded", "parser-implementations" ] num-traits = "0.2.11" [features] -#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] +#default = ["no_function", "no_index", "no_object", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] default = [] unchecked = [] # unchecked arithmetic no_stdlib = [] # no standard library of utility functions no_index = [] # no arrays and indexing no_float = [] # no floating-point no_function = [] # no script-defined functions +no_object = [] # no custom objects no_optimize = [] # no script optimizer optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simplify testing only_i32 = [] # set INT=i32 (useful for 32-bit systems) diff --git a/README.md b/README.md index 922b0969..969e727a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Optional features | `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! | | `no_function` | Disable script-defined functions if not needed. | | `no_index` | Disable arrays and indexing features if not needed. | +| `no_object` | Disable support for custom types and objects. | | `no_float` | Disable floating-point numbers and math if not needed. | | `no_optimize` | Disable the script optimizer. | | `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | @@ -76,6 +77,7 @@ Excluding unneeded functionalities can result in smaller, faster builds as well [`no_index`]: #optional-features [`no_float`]: #optional-features [`no_function`]: #optional-features +[`no_object`]: #optional-features [`no_optimize`]: #optional-features [`only_i32`]: #optional-features [`only_i64`]: #optional-features @@ -177,7 +179,7 @@ let result = engine.eval::("40 + 2")?; // return type is i64, specified let result: i64 = engine.eval("40 + 2")?; // return type is inferred to be i64 -let result = engine.eval("40 + 2")?; // returns an error because the actual return type is i64, not String +let result = engine.eval::("40 + 2")?; // returns an error because the actual return type is i64, not String ``` Evaluate a script file directly: @@ -259,20 +261,23 @@ Values and types ---------------- [`type_of()`]: #values-and-types +[`to_string()`]: #values-and-types The following primitive types are supported natively: -| Category | Equivalent Rust types | `type_of()` name | -| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------- | -| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | _same as type_ | -| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | _same as type_ | -| **Boolean value** | `bool` | `"bool"` | -| **Unicode character** | `char` | `"char"` | -| **Unicode string** | `String` (_not_ `&str`) | `"string"` | -| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | -| **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) | `()` | `"()"` | +| Category | Equivalent Rust types | `type_of()` | `to_string()` | +| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | +| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | +| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | +| **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | +| **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | +| **Unicode string** | `String` (_not_ `&str`) | `"string"` | `"hello"` etc. | +| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ? ? ? ]"` | +| **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `#{ "a": 1, "b": 2 }` | +| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | +| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | +| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | +| **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` | `""` _(empty string)_ | [`Dynamic`]: #values-and-types [`()`]: #values-and-types @@ -286,7 +291,9 @@ This is useful on some 32-bit systems where using 64-bit integers incurs a perfo If no floating-point is needed or supported, use the [`no_float`] feature to remove it. -There is a `type_of` function to detect the actual type of a value. This is useful because all variables are `Dynamic`. +The `to_string` function converts a standard type into a string for display purposes. + +The `type_of` function detects the actual type of a value. This is useful because all variables are `Dynamic`. ```rust // Use 'type_of()' to get the actual types of values @@ -294,7 +301,7 @@ type_of('c') == "char"; type_of(42) == "i64"; let x = 123; -x.type_of(); // error - 'type_of' cannot use postfix notation +x.type_of(); // <- error: 'type_of' cannot use method-call style type_of(x) == "i64"; x = 99.999; @@ -312,12 +319,13 @@ Value conversions [`to_int`]: #value-conversions [`to_float`]: #value-conversions -There is a `to_float` function to convert a supported number to an `f64`, and a `to_int` function to convert a supported number to `i64` and that's about it. -For other conversions, register custom conversion functions. +The `to_float` function converts a supported number to `FLOAT` (`f32` or `f64`), +and the `to_int` function converts a supported number to `INT` (`i32` or `i64`). +That's about it. For other conversions, register custom conversion functions. ```rust let x = 42; -let y = x * 100.0; // error: cannot multiply i64 with f64 +let y = x * 100.0; // <- error: cannot multiply i64 with f64 let y = x.to_float() * 100.0; // works let z = y.to_int() + x; // works @@ -333,8 +341,8 @@ To call these functions, they need to be registered with the [`Engine`]. ```rust use rhai::{Engine, EvalAltResult}; -use rhai::RegisterFn; // use `RegisterFn` trait for `register_fn` -use rhai::{Dynamic, RegisterDynamicFn}; // use `RegisterDynamicFn` trait for `register_dynamic_fn` +use rhai::RegisterFn; // use `RegisterFn` trait for `register_fn` +use rhai::{Dynamic, RegisterDynamicFn}; // use `RegisterDynamicFn` trait for `register_dynamic_fn` // Normal function fn add(x: i64, y: i64) -> i64 { @@ -354,14 +362,14 @@ fn main() -> Result<(), EvalAltResult> let result = engine.eval::("add(40, 2)")?; - println!("Answer: {}", result); // prints 42 + println!("Answer: {}", result); // prints 42 // Functions that return Dynamic values must use register_dynamic_fn() engine.register_dynamic_fn("get_an_any", get_an_any); let result = engine.eval::("get_an_any()")?; - println!("Answer: {}", result); // prints 42 + println!("Answer: {}", result); // prints 42 Ok(()) } @@ -415,13 +423,13 @@ The function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements ```rust use rhai::{Engine, EvalAltResult, Position}; -use rhai::RegisterResultFn; // use `RegisterResultFn` trait for `register_result_fn` +use rhai::RegisterResultFn; // use `RegisterResultFn` trait for `register_result_fn` // Function that may fail fn safe_divide(x: i64, y: i64) -> Result { if y == 0 { // Return an error if y is zero - Err("Division by zero detected!".into()) // short-cut to create EvalAltResult + Err("Division by zero detected!".into()) // short-cut to create EvalAltResult } else { Ok(x / y) } @@ -435,7 +443,7 @@ fn main() engine.register_result_fn("divide", safe_divide); if let Err(error) = engine.eval::("divide(40, 0)") { - println!("Error: {:?}", error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") + println!("Error: {:?}", error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") } } ``` @@ -496,6 +504,7 @@ fn main() -> Result<(), EvalAltResult> ``` All custom types must implement `Clone`. This allows the [`Engine`] to pass by value. +You can turn off support for custom types via the [`no_object`] feature. ```rust #[derive(Clone)] @@ -522,7 +531,8 @@ let mut engine = Engine::new(); engine.register_type::(); ``` -To use methods and functions with the [`Engine`], we need to register them. There are some convenience functions to help with this. Below I register update and new with the [`Engine`]. +To use methods and functions with the [`Engine`], we need to register them. There are some convenience functions to help with this. +Below I register update and new with the [`Engine`]. *Note: [`Engine`] follows the convention that methods use a `&mut` first parameter so that invoking methods can update the value in memory.* @@ -531,7 +541,8 @@ engine.register_fn("update", TestStruct::update); // registers 'update(&mut ts engine.register_fn("new_ts", TestStruct::new); // registers 'new' ``` -Finally, we call our script. The script can see the function and method we registered earlier. We need to get the result back out from script land just as before, this time casting to our custom struct type. +Finally, we call our script. The script can see the function and method we registered earlier. +We need to get the result back out from script land just as before, this time casting to our custom struct type. ```rust let result = engine.eval::("let x = new_ts(); x.update(); x")?; @@ -539,7 +550,8 @@ let result = engine.eval::("let x = new_ts(); x.update(); x")?; println!("result: {}", result.field); // prints 42 ``` -In fact, any function with a first argument (either by copy or via a `&mut` reference) can be used as a method-call on that type because internally they are the same thing: methods on a type is implemented as a functions taking an first argument. +In fact, any function with a first argument (either by copy or via a `&mut` reference) can be used as a method-call on that type because internally they are the same thing: +methods on a type is implemented as a functions taking an first argument. ```rust fn foo(ts: &mut TestStruct) -> i64 { @@ -553,7 +565,15 @@ let result = engine.eval::("let x = new_ts(); x.foo()")?; println!("result: {}", result); // prints 1 ``` -[`type_of()`] works fine with custom types and returns the name of the type. If `register_type_with_name` is used to register the custom type +If the [`no_object`] feature is turned on, however, the _method_ style of function calls (i.e. calling a function as an object-method) is no longer supported. + +```rust +// Below is a syntax error under 'no_object' because 'len' cannot be called in method style. +let result = engine.eval::("let x = [1, 2, 3]; x.len()")?; +``` + +[`type_of()`] works fine with custom types and returns the name of the type. +If `register_type_with_name` is used to register the custom type with a special "pretty-print" name, [`type_of()`] will return that name instead. ```rust @@ -605,6 +625,9 @@ let result = engine.eval::("let a = new_ts(); a.xyz = 42; a.xyz")?; println!("Answer: {}", result); // prints 42 ``` +Needless to say, `register_type`, `register_type_with_name`, `register_get`, `register_set` and `register_get_set` +are not available when the [`no_object`] feature is turned on. + Initializing and maintaining state --------------------------------- @@ -711,6 +734,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. @@ -720,41 +745,41 @@ Variable names are also case _sensitive_. Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. ```rust -let x = 3; // ok -let _x = 42; // ok -let x_ = 42; // also ok -let _x_ = 42; // still ok +let x = 3; // ok +let _x = 42; // ok +let x_ = 42; // also ok +let _x_ = 42; // still ok -let _ = 123; // syntax error - illegal variable name -let _9 = 9; // syntax error - illegal variable name +let _ = 123; // <- syntax error: illegal variable name +let _9 = 9; // <- syntax error: illegal variable name -let x = 42; // variable is 'x', lower case -let X = 123; // variable is 'X', upper case +let x = 42; // variable is 'x', lower case +let X = 123; // variable is 'X', upper case x == 42; X == 123; { - let x = 999; // local variable 'x' shadows the 'x' in parent block - x == 999; // access to local 'x' + let x = 999; // local variable 'x' shadows the 'x' in parent block + x == 999; // access to local 'x' } -x == 42; // the parent block's 'x' is not changed +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; -print(x * 2); // prints 84 -x = 123; // syntax error - cannot assign to constant +print(x * 2); // prints 84 +x = 123; // <- syntax error: cannot assign to constant ``` Constants must be assigned a _value_, not an expression. ```rust -const x = 40 + 2; // syntax error - cannot assign expression to constant +const x = 40 + 2; // <- syntax error: cannot assign expression to constant ``` Numbers @@ -982,7 +1007,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]; @@ -1023,6 +1048,84 @@ 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 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`. + +[`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, // 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; // 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["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; +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; + +y.xyz == (); // A non-existing property returns '()' +y["xyz"] == (); + +print(y.len()); // prints 3 + +y.clear(); // empty the object map + +print(y.len()); // prints 0 +``` + Comparison operators -------------------- @@ -1032,21 +1135,21 @@ However, if the [`no_stdlib`] feature is turned on, comparisons can only be made types - `INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), string, array, `bool`, `char`. ```rust -42 == 42; // true -42 > 42; // false -"hello" > "foo"; // true -"42" == 42; // false +42 == 42; // true +42 > 42; // false +"hello" > "foo"; // true +"42" == 42; // false ``` Comparing two values of _different_ data types, or of unknown data types, always results in `false`. ```rust -42 == 42.0; // false - i64 is different from f64 -42 > "42"; // false - i64 is different from string -42 <= "42"; // false again +42 == 42.0; // false - i64 is different from f64 +42 > "42"; // false - i64 is different from string +42 <= "42"; // false again -let ts = new_ts(); // custom type -ts == 42; // false - types are not the same +let ts = new_ts(); // custom type +ts == 42; // false - types are not the same ``` Boolean operators @@ -1066,11 +1169,11 @@ if the first one already proves the condition wrong. Single boolean operators `&` and `|` always evaluate both operands. ```rust -this() || that(); // that() is not evaluated if this() is true -this() && that(); // that() is not evaluated if this() is false +this() || that(); // that() is not evaluated if this() is true +this() && that(); // that() is not evaluated if this() is false -this() | that(); // both this() and that() are evaluated -this() & that(); // both this() and that() are evaluated +this() | that(); // both this() and that() are evaluated +this() & that(); // both this() and that() are evaluated ``` Compound assignment operators @@ -1078,13 +1181,13 @@ Compound assignment operators ```rust let number = 5; -number += 4; // number = number + 4 -number -= 3; // number = number - 3 -number *= 2; // number = number * 2 -number /= 1; // number = number / 1 -number %= 3; // number = number % 3 -number <<= 2; // number = number << 2 -number >>= 1; // number = number >> 1 +number += 4; // number = number + 4 +number -= 3; // number = number - 3 +number *= 2; // number = number * 2 +number /= 1; // number = number / 1 +number %= 3; // number = number % 3 +number <<= 2; // number = number << 2 +number >>= 1; // number = number >> 1 ``` The `+=` operator can also be used to build strings: @@ -1183,9 +1286,9 @@ for x in range(0, 50) { ------------------- ```rust -return; // equivalent to return (); +return; // equivalent to return (); -return 123 + 456; // returns 579 +return 123 + 456; // returns 579 ``` Errors and `throw`-ing exceptions @@ -1196,10 +1299,10 @@ To deliberately return an error during an evaluation, use the `throw` keyword. ```rust if some_bad_condition_has_happened { - throw error; // 'throw' takes a string to form the exception text + throw error; // 'throw' takes a string as the exception text } -throw; // empty exception text: "" +throw; // defaults to empty exception text: "" ``` Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(`_reason_`, `_position_`))` @@ -1214,7 +1317,7 @@ let result = engine.eval::(r#" } "#); -println!(result); // prints "Runtime error: 42 is too large! (line 5, position 15)" +println!(result); // prints "Runtime error: 42 is too large! (line 5, position 15)" ``` Functions @@ -1236,16 +1339,17 @@ Just like in Rust, an implicit return can be used. In fact, the last statement o regardless of whether it is terminated with a semicolon `';'`. This is different from Rust. ```rust -fn add(x, y) { - x + y; // value of the last statement (no need for ending semicolon) is used as the return value +fn add(x, y) { // implicit return: + x + y; // value of the last statement (no need for ending semicolon) + // is used as the return value } fn add2(x) { - return x + 2; // explicit return + return x + 2; // explicit return } -print(add(2, 3)); // prints 5 -print(add2(42)); // prints 44 +print(add(2, 3)); // prints 5 +print(add2(42)); // prints 44 ``` ### No access to external scope @@ -1255,7 +1359,7 @@ Functions can only access their parameters. They cannot access external variabl ```rust let x = 42; -fn foo() { x } // syntax error - variable 'x' doesn't exist +fn foo() { x } // <- syntax error: variable 'x' doesn't exist ``` ### Passing arguments by value @@ -1265,13 +1369,13 @@ It is important to remember that all arguments are passed by _value_, so all fun Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if not careful. ```rust -fn change(s) { // 's' is passed by value - s = 42; // only a COPY of 's' is changed +fn change(s) { // 's' is passed by value + s = 42; // only a COPY of 's' is changed } let x = 500; -x.change(); // de-sugars to change(x) -x == 500; // 'x' is NOT changed! +x.change(); // de-sugars to change(x) +x == 500; // 'x' is NOT changed! ``` ### Global definitions only @@ -1286,7 +1390,7 @@ fn add(x, y) { // The following will not compile fn do_addition(x) { - fn add_y(n) { // functions cannot be defined inside another function + fn add_y(n) { // functions cannot be defined inside another function n + y } @@ -1307,10 +1411,10 @@ fn foo(x,y) { print("Two! " + x + "," + y) } fn foo() { print("None.") } fn foo(x) { print("HA! NEW ONE! " + x) } // overwrites previous definition -foo(1,2,3); // prints "Three!!! 1,2,3" -foo(42); // prints "HA! NEW ONE! 42" -foo(1,2); // prints "Two!! 1,2" -foo(); // prints "None." +foo(1,2,3); // prints "Three!!! 1,2,3" +foo(42); // prints "HA! NEW ONE! 42" +foo(1,2); // prints "Two!! 1,2" +foo(); // prints "None." ``` Members and methods @@ -1319,23 +1423,26 @@ Members and methods Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like in Rust. ```rust -let a = new_ts(); // constructor function -a.field = 500; // property access -a.update(); // method call +let a = new_ts(); // constructor function +a.field = 500; // property access +a.update(); // method call -update(a); // this works, but 'a' is unchanged because only a COPY of 'a' is passed to 'update' by VALUE +update(a); // this works, but 'a' is unchanged because only + // a COPY of 'a' is passed to 'update' by VALUE ``` +Custom types, properties and methods can be disabled via the [`no_object`] feature. + `print` and `debug` ------------------- The `print` and `debug` functions default to printing to `stdout`, with `debug` using standard debug formatting. ```rust -print("hello"); // prints hello to stdout -print(1 + 2 + 3); // prints 6 to stdout -print("hello" + 42); // prints hello42 to stdout -debug("world!"); // prints "world!" to stdout using debug formatting +print("hello"); // prints hello to stdout +print(1 + 2 + 3); // prints 6 to stdout +print("hello" + 42); // prints hello42 to stdout +debug("world!"); // prints "world!" to stdout using debug formatting ``` ### Overriding `print` and `debug` with callback functions @@ -1376,14 +1483,13 @@ For example, in the following: ```rust { - let x = 999; // NOT eliminated - Rhai doesn't check yet whether a variable is used later on - 123; // eliminated - no effect - "hello"; // eliminated - no effect - [1, 2, x, x*2, 5]; // eliminated - no effect - foo(42); // NOT eliminated - the function 'foo' may have side effects - 666 // NOT eliminated - this is the return value of the block, - // and the block is the last one - // so this is the return value of the whole script + let x = 999; // NOT eliminated: Rhai doesn't check yet whether a variable is used later on + 123; // eliminated: no effect + "hello"; // eliminated: no effect + [1, 2, x, x*2, 5]; // eliminated: no effect + foo(42); // NOT eliminated: the function 'foo' may have side effects + 666 // NOT eliminated: this is the return value of the block, + // and the block is the last one so this is the return value of the whole script } ``` @@ -1406,7 +1512,7 @@ if ABC || some_work() { print("done!"); } // 'ABC' is constant so it is replac if true || some_work() { print("done!"); } // since '||' short-circuits, 'some_work' is never called if true { print("done!"); } // <- the line above is equivalent to this print("done!"); // <- the line above is further simplified to this - // because the condition is always true + // because the condition is always true ``` These are quite effective for template-based machine-generated scripts where certain constant values @@ -1421,7 +1527,7 @@ so they are not optimized away: const DECISION = 1; if DECISION == 1 { // NOT optimized away because you can define - : // your own '==' function to override the built-in default! + : // your own '==' function to override the built-in default! : } else if DECISION == 2 { // same here, NOT optimized away : @@ -1491,14 +1597,15 @@ evaluated all function calls with constant arguments, using the result to replac // When compiling the following with OptimizationLevel::Full... const DECISION = 1; - // this condition is now eliminated because 'DECISION == 1' -if DECISION == 1 { // is a function call to the '==' function, and it returns 'true' - print("hello!"); // this block is promoted to the parent level + // this condition is now eliminated because 'DECISION == 1' +if DECISION == 1 { // is a function call to the '==' function, and it returns 'true' + print("hello!"); // this block is promoted to the parent level } else { - print("boo!"); // this block is eliminated because it is never reached + print("boo!"); // this block is eliminated because it is never reached } -print("hello!"); // <- the above is equivalent to this ('print' and 'debug' are handled specially) +print("hello!"); // <- the above is equivalent to this + // ('print' and 'debug' are handled specially) ``` Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result. @@ -1507,8 +1614,8 @@ This does not happen with [`OptimizationLevel::Simple`] which doesn't assume all ```rust // When compiling the following with OptimizationLevel::Full... -let x = (1 + 2) * 3 - 4 / 5 % 6; // <- will be replaced by 'let x = 9' -let y = (1 > 2) || (3 <= 4); // <- will be replaced by 'let y = true' +let x = (1+2)*3-4/5%6; // <- will be replaced by 'let x = 9' +let y = (1>2) || (3<=4); // <- will be replaced by 'let y = true' ``` Function side effect considerations @@ -1539,13 +1646,13 @@ Subtle semantic changes Some optimizations can alter subtle semantics of the script. For example: ```rust -if true { // condition always true - 123.456; // eliminated - hello; // eliminated, EVEN THOUGH the variable doesn't exist! - foo(42) // promoted up-level +if true { // condition always true + 123.456; // eliminated + hello; // eliminated, EVEN THOUGH the variable doesn't exist! + foo(42) // promoted up-level } -foo(42) // <- the above optimizes to this +foo(42) // <- the above optimizes to this ``` Nevertheless, if the original script were evaluated instead, it would have been an error - the variable `hello` doesn't exist, @@ -1591,22 +1698,22 @@ let x = 10; fn foo(x) { x += 12; x } -let script = "let y = x;"; // build a script +let script = "let y = x;"; // build a script script += "y += foo(y);"; script += "x + y"; -let result = eval(script); // <- look, JS, we can also do this! +let result = eval(script); // <- look, JS, we can also do this! -print("Answer: " + result); // prints 42 +print("Answer: " + result); // prints 42 -print("x = " + x); // prints 10 - functions call arguments are passed by value -print("y = " + y); // prints 32 - variables defined in 'eval' persist! +print("x = " + x); // prints 10: functions call arguments are passed by value +print("y = " + y); // prints 32: variables defined in 'eval' persist! -eval("{ let z = y }"); // to keep a variable local, use a statement block +eval("{ let z = y }"); // to keep a variable local, use a statement block -print("z = " + z); // error - variable 'z' not found +print("z = " + z); // <- error: variable 'z' not found -"print(42)".eval(); // nope - just like 'type_of' postfix notation doesn't work +"print(42)".eval(); // <- nope... just like 'type_of', method-call style doesn't work ``` Script segments passed to `eval` execute inside the current [`Scope`], so they can access and modify _everything_, @@ -1616,8 +1723,8 @@ physically pasted in at the position of the `eval` call. ```rust let script = "x += 32"; let x = 10; -eval(script); // variable 'x' in the current scope is visible! -print(x); // prints 42 +eval(script); // variable 'x' in the current scope is visible! +print(x); // prints 42 // The above is equivalent to: let script = "x += 32"; @@ -1632,7 +1739,7 @@ disable `eval` by overriding it, probably with something that throws. ```rust fn eval(script) { throw "eval is evil! I refuse to run " + script } -let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2" +let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2" ``` Or override it from Rust: diff --git a/examples/arrays_and_structs.rs b/examples/arrays_and_structs.rs index 8ea7fcf0..5e604120 100644 --- a/examples/arrays_and_structs.rs +++ b/examples/arrays_and_structs.rs @@ -15,6 +15,8 @@ impl TestStruct { } } +#[cfg(not(feature = "no_index"))] +#[cfg(not(feature = "no_object"))] fn main() { let mut engine = Engine::new(); @@ -32,3 +34,6 @@ fn main() { engine.eval::("let x = [new_ts()]; x[0].update(); x[0]") ); } + +#[cfg(any(feature = "no_index", feature = "no_object"))] +fn main() {} diff --git a/examples/custom_types_and_methods.rs b/examples/custom_types_and_methods.rs index bd20159e..c912520d 100644 --- a/examples/custom_types_and_methods.rs +++ b/examples/custom_types_and_methods.rs @@ -15,6 +15,7 @@ impl TestStruct { } } +#[cfg(not(feature = "no_object"))] fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); @@ -29,3 +30,6 @@ fn main() -> Result<(), EvalAltResult> { Ok(()) } + +#[cfg(feature = "no_object")] +fn main() {} diff --git a/src/api.rs b/src/api.rs index db92aed7..7e010192 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,7 +2,7 @@ use crate::any::{Any, AnyExt, Dynamic}; use crate::call::FuncArgs; -use crate::engine::{Engine, FnAny, FnSpec, FUNC_GETTER, FUNC_SETTER}; +use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec}; use crate::error::ParseError; use crate::fn_register::RegisterFn; use crate::parser::{lex, parse, parse_global_expr, FnDef, Position, AST}; @@ -15,7 +15,6 @@ use crate::optimize::optimize_into_ast; use crate::stdlib::{ any::{type_name, TypeId}, boxed::Box, - format, string::{String, ToString}, sync::Arc, vec::Vec, @@ -25,12 +24,7 @@ use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; impl<'e> Engine<'e> { /// Register a custom function. - pub(crate) fn register_fn_raw( - &mut self, - fn_name: &str, - args: Option>, - f: Box, - ) { + pub(crate) fn register_fn_raw(&mut self, fn_name: &str, args: Vec, f: Box) { let spec = FnSpec { name: fn_name.to_string().into(), args, @@ -45,7 +39,7 @@ impl<'e> Engine<'e> { /// # Example /// /// ``` - /// #[derive(Clone)] + /// #[derive(Debug, Clone, Eq, PartialEq)] /// struct TestStruct { /// field: i64 /// } @@ -69,12 +63,13 @@ impl<'e> Engine<'e> { /// engine.register_fn("update", TestStruct::update); /// /// assert_eq!( - /// engine.eval::("let x = new_ts(); x.update(41); x")?.field, - /// 42 + /// engine.eval::("let x = new_ts(); x.update(41); x")?, + /// TestStruct { field: 42 } /// ); /// # Ok(()) /// # } /// ``` + #[cfg(not(feature = "no_object"))] pub fn register_type(&mut self) { self.register_type_with_name::(type_name::()); } @@ -86,7 +81,7 @@ impl<'e> Engine<'e> { /// /// ``` /// #[derive(Clone)] - /// struct TestStruct { + /// struct TestStruct { /// field: i64 /// } /// @@ -122,6 +117,7 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` + #[cfg(not(feature = "no_object"))] pub fn register_type_with_name(&mut self, name: &str) { // Add the pretty-print type name into the map self.type_names @@ -145,7 +141,7 @@ impl<'e> Engine<'e> { /// /// ``` /// #[derive(Clone)] - /// struct TestStruct { + /// struct TestStruct { /// field: i64 /// } /// @@ -173,13 +169,13 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` + #[cfg(not(feature = "no_object"))] pub fn register_get( &mut self, name: &str, callback: impl Fn(&mut T) -> U + 'static, ) { - let get_fn_name = format!("{}{}", FUNC_GETTER, name); - self.register_fn(&get_fn_name, callback); + self.register_fn(&make_getter(name), callback); } /// Register a setter function for a member of a registered type with the `Engine`. @@ -187,7 +183,7 @@ impl<'e> Engine<'e> { /// # Example /// /// ``` - /// #[derive(Clone)] + /// #[derive(Debug, Clone, Eq, PartialEq)] /// struct TestStruct { /// field: i64 /// } @@ -211,17 +207,20 @@ impl<'e> Engine<'e> { /// engine.register_set("xyz", TestStruct::set_field); /// /// // Notice that, with a getter, there is no way to get the property value - /// engine.eval("let a = new_ts(); a.xyz = 42;")?; + /// assert_eq!( + /// engine.eval::("let a = new_ts(); a.xyz = 42; a")?, + /// TestStruct { field: 42 } + /// ); /// # Ok(()) /// # } /// ``` + #[cfg(not(feature = "no_object"))] pub fn register_set( &mut self, name: &str, callback: impl Fn(&mut T, U) -> () + 'static, ) { - let set_fn_name = format!("{}{}", FUNC_SETTER, name); - self.register_fn(&set_fn_name, callback); + self.register_fn(&make_setter(name), callback); } /// Shorthand for registering both getter and setter functions @@ -262,6 +261,7 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` + #[cfg(not(feature = "no_object"))] pub fn register_get_set( &mut self, name: &str, @@ -346,8 +346,9 @@ impl<'e> Engine<'e> { let mut contents = String::new(); f.read_to_string(&mut contents) - .map_err(|err| EvalAltResult::ErrorReadingScriptFile(path.clone(), err)) - .map(|_| contents) + .map_err(|err| EvalAltResult::ErrorReadingScriptFile(path.clone(), err))?; + + Ok(contents) } /// Compile a script file into an `AST`, which can be used later for evaluation. @@ -552,8 +553,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn eval(&mut self, input: &str) -> Result { - let mut scope = Scope::new(); - self.eval_with_scope(&mut scope, input) + self.eval_with_scope(&mut Scope::new(), input) } /// Evaluate a string with own scope. @@ -602,8 +602,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn eval_expression(&mut self, input: &str) -> Result { - let mut scope = Scope::new(); - self.eval_expression_with_scope(&mut scope, input) + self.eval_expression_with_scope(&mut Scope::new(), input) } /// Evaluate a string containing an expression with own scope. @@ -655,8 +654,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn eval_ast(&mut self, ast: &AST) -> Result { - let mut scope = Scope::new(); - self.eval_ast_with_scope(&mut scope, ast) + self.eval_ast_with_scope(&mut Scope::new(), ast) } /// Evaluate an `AST` with own scope. @@ -693,14 +691,14 @@ impl<'e> Engine<'e> { scope: &mut Scope, ast: &AST, ) -> Result { - self.eval_ast_with_scope_raw(scope, false, ast) - .and_then(|out| { - out.downcast::().map(|v| *v).map_err(|a| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name((*a).type_name()).to_string(), - Position::none(), - ) - }) + self.eval_ast_with_scope_raw(scope, false, ast)? + .downcast::() + .map(|v| *v) + .map_err(|a| { + EvalAltResult::ErrorMismatchOutputType( + self.map_type_name((*a).type_name()).to_string(), + Position::none(), + ) }) } @@ -710,34 +708,25 @@ impl<'e> Engine<'e> { retain_functions: bool, ast: &AST, ) -> Result { - fn eval_ast_internal( - engine: &mut Engine, - scope: &mut Scope, - retain_functions: bool, - ast: &AST, - ) -> Result { - if !retain_functions { - engine.clear_functions(); - } - - let statements = { - let AST(statements, functions) = ast; - engine.load_script_functions(functions); - statements - }; - - let result = statements.iter().try_fold(().into_dynamic(), |_, stmt| { - engine.eval_stmt(scope, stmt, 0) - })?; - - if !retain_functions { - engine.clear_functions(); - } - - Ok(result) + if !retain_functions { + self.clear_functions(); } - eval_ast_internal(self, scope, retain_functions, ast).or_else(|err| match err { + let statements = { + let AST(statements, functions) = ast; + self.load_script_functions(functions); + statements + }; + + let result = statements + .iter() + .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt, 0)); + + if !retain_functions { + self.clear_functions(); + } + + result.or_else(|err| match err { EvalAltResult::Return(out, _) => Ok(out), _ => Err(err), }) @@ -746,7 +735,9 @@ impl<'e> Engine<'e> { /// Evaluate a file, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. + /// # Note + /// + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. #[cfg(not(feature = "no_std"))] pub fn consume_file( &mut self, @@ -759,7 +750,9 @@ impl<'e> Engine<'e> { /// Evaluate a file with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. + /// # Note + /// + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. #[cfg(not(feature = "no_std"))] pub fn consume_file_with_scope( &mut self, @@ -774,7 +767,9 @@ impl<'e> Engine<'e> { /// Evaluate a string, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// # Note + /// + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> { self.consume_with_scope(&mut Scope::new(), retain_functions, input) } @@ -782,7 +777,9 @@ impl<'e> Engine<'e> { /// Evaluate a string with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// # Note + /// + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume_with_scope( &mut self, scope: &mut Scope, @@ -800,7 +797,9 @@ impl<'e> Engine<'e> { /// Evaluate an AST, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// # Note + /// + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> { self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast) } @@ -808,7 +807,9 @@ impl<'e> Engine<'e> { /// Evaluate an `AST` with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// # Note + /// + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume_ast_with_scope( &mut self, scope: &mut Scope, @@ -827,14 +828,13 @@ impl<'e> Engine<'e> { let result = statements .iter() - .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt, 0)) - .map(|_| ()); + .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt, 0)); if !retain_functions { self.clear_functions(); } - result.or_else(|err| match err { + result.map(|_| ()).or_else(|err| match err { EvalAltResult::Return(_, _) => Ok(()), _ => Err(err), }) @@ -845,9 +845,9 @@ impl<'e> Engine<'e> { &mut self, functions: impl IntoIterator>, ) { - for f in functions.into_iter() { - self.fn_lib.add_or_replace_function(f.clone()); - } + functions.into_iter().cloned().for_each(|f| { + self.fn_lib.add_or_replace_function(f); + }); } /// Call a script function retained inside the Engine. @@ -864,7 +864,7 @@ impl<'e> Engine<'e> { /// let mut engine = Engine::new(); /// /// // Set 'retain_functions' in 'consume' to keep the function definitions - /// engine.consume(true, "fn add(x, y) { x.len() + y }")?; + /// engine.consume(true, "fn add(x, y) { len(x) + y }")?; /// /// // Call the script-defined function /// let result: i64 = engine.call_fn("add", (String::from("abc"), 123_i64))?; @@ -883,14 +883,14 @@ impl<'e> Engine<'e> { let mut values = args.into_vec(); let mut arg_values: Vec<_> = values.iter_mut().map(Dynamic::as_mut).collect(); - self.call_fn_raw(name, &mut arg_values, None, Position::none(), 0) - .and_then(|b| { - b.downcast().map(|b| *b).map_err(|a| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name((*a).type_name()).into(), - Position::none(), - ) - }) + self.call_fn_raw(name, &mut arg_values, None, Position::none(), 0)? + .downcast() + .map(|b| *b) + .map_err(|a| { + EvalAltResult::ErrorMismatchOutputType( + self.map_type_name((*a).type_name()).into(), + Position::none(), + ) }) } diff --git a/src/builtin.rs b/src/builtin.rs index 96aab8c2..ae5feb13 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -2,7 +2,7 @@ //! _standard library_ of utility functions. use crate::any::Any; -use crate::engine::Engine; +use crate::engine::{Engine, FUNC_TO_STRING, KEYWORD_DEBUG, KEYWORD_PRINT}; use crate::fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; use crate::parser::{Position, INT}; use crate::result::EvalAltResult; @@ -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; @@ -558,10 +561,10 @@ impl Engine<'_> { self.register_fn("==", |_: (), _: ()| true); // () == () // Register print and debug - fn debug(x: T) -> String { + fn to_debug(x: T) -> String { format!("{:?}", x) } - fn print(x: T) -> String { + fn to_string(x: T) -> String { format!("{}", x) } @@ -574,36 +577,58 @@ impl Engine<'_> { ) } - reg_fn1!(self, "print", print, String, INT, bool, char, String); - self.register_fn("print", || "".to_string()); - self.register_fn("print", |_: ()| "".to_string()); - reg_fn1!(self, "debug", debug, String, INT, bool, char, String, ()); + reg_fn1!(self, KEYWORD_PRINT, to_string, String, INT, bool); + reg_fn1!(self, FUNC_TO_STRING, to_string, String, INT, bool); + reg_fn1!(self, KEYWORD_PRINT, to_string, String, char, String); + reg_fn1!(self, FUNC_TO_STRING, to_string, String, char, String); + self.register_fn(KEYWORD_PRINT, || "".to_string()); + self.register_fn(KEYWORD_PRINT, |_: ()| "".to_string()); + self.register_fn(FUNC_TO_STRING, |_: ()| "".to_string()); + reg_fn1!(self, KEYWORD_DEBUG, to_debug, String, INT, bool, ()); + reg_fn1!(self, KEYWORD_DEBUG, to_debug, String, char, String); #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] { - reg_fn1!(self, "print", print, String, i8, u8, i16, u16); - reg_fn1!(self, "print", print, String, i32, i64, u32, u64); - reg_fn1!(self, "debug", debug, String, i8, u8, i16, u16); - reg_fn1!(self, "debug", debug, String, i32, i64, u32, u64); + reg_fn1!(self, KEYWORD_PRINT, to_string, String, i8, u8, i16, u16); + reg_fn1!(self, FUNC_TO_STRING, to_string, String, i8, u8, i16, u16); + reg_fn1!(self, KEYWORD_PRINT, to_string, String, i32, i64, u32, u64); + reg_fn1!(self, FUNC_TO_STRING, to_string, String, i32, i64, u32, u64); + reg_fn1!(self, KEYWORD_DEBUG, to_debug, String, i8, u8, i16, u16); + reg_fn1!(self, KEYWORD_DEBUG, to_debug, String, i32, i64, u32, u64); } #[cfg(not(feature = "no_float"))] { - reg_fn1!(self, "print", print, String, f32, f64); - reg_fn1!(self, "debug", debug, String, f32, f64); + reg_fn1!(self, KEYWORD_PRINT, to_string, String, f32, f64); + reg_fn1!(self, FUNC_TO_STRING, to_string, String, f32, f64); + reg_fn1!(self, KEYWORD_DEBUG, to_debug, String, f32, f64); } #[cfg(not(feature = "no_index"))] { - reg_fn1!(self, "print", debug, String, Array); - reg_fn1!(self, "debug", debug, String, Array); + reg_fn1!(self, KEYWORD_PRINT, to_debug, String, Array); + reg_fn1!(self, FUNC_TO_STRING, to_debug, String, Array); + reg_fn1!(self, KEYWORD_DEBUG, to_debug, String, Array); // Register array iterator self.register_iterator::(|a| { Box::new(a.downcast_ref::().unwrap().clone().into_iter()) }); } + + #[cfg(not(feature = "no_object"))] + { + self.register_fn(KEYWORD_PRINT, |x: &mut Map| -> String { + format!("#{:?}", x) + }); + self.register_fn(FUNC_TO_STRING, |x: &mut Map| -> String { + format!("#{:?}", x) + }); + self.register_fn(KEYWORD_DEBUG, |x: &mut Map| -> String { + format!("#{:?}", x) + }); + } } // Register range function @@ -822,6 +847,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 9ce74b9d..b20cfabd 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -26,33 +26,88 @@ 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; type IteratorFn = dyn Fn(&Dynamic) -> Box>; -pub(crate) const MAX_CALL_STACK_DEPTH: usize = 64; -pub(crate) const KEYWORD_PRINT: &str = "print"; -pub(crate) const KEYWORD_DEBUG: &str = "debug"; -pub(crate) const KEYWORD_DUMP_AST: &str = "dump_ast"; -pub(crate) const KEYWORD_TYPE_OF: &str = "type_of"; -pub(crate) const KEYWORD_EVAL: &str = "eval"; -pub(crate) const FUNC_GETTER: &str = "get$"; -pub(crate) const FUNC_SETTER: &str = "set$"; +pub const MAX_CALL_STACK_DEPTH: usize = 64; +pub const KEYWORD_PRINT: &str = "print"; +pub const KEYWORD_DEBUG: &str = "debug"; +pub const KEYWORD_DUMP_AST: &str = "dump_ast"; +pub const KEYWORD_TYPE_OF: &str = "type_of"; +pub const KEYWORD_EVAL: &str = "eval"; +pub const FUNC_TO_STRING: &str = "to_string"; +pub const FUNC_GETTER: &str = "get$"; +pub const FUNC_SETTER: &str = "set$"; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] #[cfg(not(feature = "no_index"))] enum IndexSourceType { - Array, - String, Expression, + String, + Array, + #[cfg(not(feature = "no_object"))] + Map, +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +enum IndexValue { + Num(usize), + Str(String), +} + +impl IndexValue { + fn from_num(idx: INT) -> Self { + Self::Num(idx as usize) + } + fn from_str(name: String) -> Self { + Self::Str(name) + } + fn as_num(self) -> usize { + match self { + Self::Num(n) => n, + _ => panic!("index value is numeric"), + } + } + fn as_str(self) -> String { + match self { + Self::Str(s) => s, + _ => panic!("index value is string"), + } + } +} + +#[derive(Debug)] +enum Target<'a> { + Scope(ScopeSource<'a>), + Value(&'a mut Variant), +} + +impl<'a> Target<'a> { + fn from(value: &'a mut Variant) -> Self { + Self::Value(value) + } + fn from_src(src: ScopeSource<'a>) -> Self { + Self::Scope(src) + } + fn get_mut(self, scope: &'a mut Scope) -> &'a mut Variant { + match self { + Self::Value(t) => t, + Self::Scope(src) => scope.get_mut(src).as_mut(), + } + } } #[derive(Debug, Eq, PartialEq, Hash, Clone)] pub struct FnSpec<'a> { pub name: Cow<'a, str>, - pub args: Option>, + pub args: Vec, } /// A type that holds a library of script-defined functions. @@ -137,9 +192,9 @@ pub struct Engine<'e> { /// A hashmap mapping type names to pretty-print names. pub(crate) type_names: HashMap, - /// Closure for implementing the print commands. + /// Closure for implementing the `print` command. pub(crate) on_print: Box, - /// Closure for implementing the debug commands. + /// Closure for implementing the `debug` command. pub(crate) on_debug: Box, /// Optimize the AST after compilation. @@ -156,6 +211,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"), ] @@ -192,6 +249,34 @@ impl Default for Engine<'_> { } } +/// Make getter function +pub fn make_getter(id: &str) -> String { + format!("{}{}", FUNC_GETTER, id) +} + +/// Extract the property name from a getter function name. +fn extract_prop_from_getter(fn_name: &str) -> Option<&str> { + if fn_name.starts_with(FUNC_GETTER) { + Some(&fn_name[FUNC_GETTER.len()..]) + } else { + None + } +} + +/// Make setter function +pub fn make_setter(id: &str) -> String { + format!("{}{}", FUNC_SETTER, id) +} + +/// Extract the property name from a setter function name. +fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { + if fn_name.starts_with(FUNC_SETTER) { + Some(&fn_name[FUNC_SETTER.len()..]) + } else { + None + } +} + impl Engine<'_> { /// Create a new `Engine` pub fn new() -> Self { @@ -220,7 +305,7 @@ impl Engine<'_> { ) -> Result, EvalAltResult> { let spec = FnSpec { name: fn_name.into(), - args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()), + args: args.iter().map(|a| Any::type_id(&**a)).collect(), }; // Search built-in's and external functions @@ -232,8 +317,7 @@ impl Engine<'_> { } } - /// Universal method for calling functions, that are either - /// registered with the `Engine` or written in Rhai + /// Universal method for calling functions either registered with the `Engine` or written in Rhai pub(crate) fn call_fn_raw( &mut self, fn_name: &str, @@ -267,7 +351,7 @@ impl Engine<'_> { let spec = FnSpec { name: fn_name.into(), - args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()), + args: args.iter().map(|a| Any::type_id(&**a)).collect(), }; // Argument must be a string @@ -285,35 +369,46 @@ impl Engine<'_> { // See if the function match print/debug (which requires special processing) return Ok(match fn_name { KEYWORD_PRINT => { - self.on_print.as_mut()(cast_to_string(result.as_ref(), pos)?); - ().into_dynamic() + self.on_print.as_mut()(cast_to_string(result.as_ref(), pos)?).into_dynamic() } KEYWORD_DEBUG => { - self.on_debug.as_mut()(cast_to_string(result.as_ref(), pos)?); - ().into_dynamic() + self.on_debug.as_mut()(cast_to_string(result.as_ref(), pos)?).into_dynamic() } _ => result, }); } - if fn_name.starts_with(FUNC_GETTER) { + if let Some(prop) = extract_prop_from_getter(fn_name) { + #[cfg(not(feature = "no_object"))] + { + // Map property access + if let Some(map) = args[0].downcast_ref::() { + return Ok(map.get(prop).cloned().unwrap_or_else(|| ().into_dynamic())); + } + } + // Getter function not found return Err(EvalAltResult::ErrorDotExpr( - format!( - "- property '{}' unknown or write-only", - &fn_name[FUNC_GETTER.len()..] - ), + format!("- property '{}' unknown or write-only", prop), pos, )); } - if fn_name.starts_with(FUNC_SETTER) { + if let Some(prop) = extract_prop_from_setter(fn_name) { + #[cfg(not(feature = "no_object"))] + { + let value = args[1].into_dynamic(); + + // Map property update + if let Some(map) = args[0].downcast_mut::() { + map.insert(prop.to_string(), value); + return Ok(().into_dynamic()); + } + } + // Setter function not found return Err(EvalAltResult::ErrorDotExpr( - format!( - "- property '{}' unknown or read-only", - &fn_name[FUNC_SETTER.len()..] - ), + format!("- property '{}' unknown or read-only", prop), pos, )); } @@ -337,39 +432,14 @@ impl Engine<'_> { } /// Chain-evaluate a dot setter. - /// - /// Either `src` or `target` should be `Some`. - /// - /// If `target` is `Some`, then it is taken as the reference to use for `this`. - /// - /// Otherwise, if `src` is `Some`, then it holds a name and index into `scope`; using `get_mut` on - /// `scope` can retrieve a mutable reference to the variable's value to use as `this`. + #[cfg(not(feature = "no_object"))] fn get_dot_val_helper( &mut self, scope: &mut Scope, - src: Option, - target: Option<&mut Variant>, + target: Target, dot_rhs: &Expr, level: usize, ) -> Result { - // Get the `this` reference. Either `src` or `target` should be `Some`. - fn get_this_ptr<'a>( - scope: &'a mut Scope, - src: Option, - target: Option<&'a mut Variant>, - ) -> &'a mut Variant { - if let Some(t) = target { - // If `target` is `Some`, then it is returned. - t - } else { - // Otherwise, if `src` is `Some`, then it holds a name and index into `scope`; - // using `get_mut` on `scope` to retrieve a mutable reference for return. - scope - .get_mut(src.expect("expected source in scope")) - .as_mut() - } - } - match dot_rhs { // xxx.fn_name(args) Expr::FunctionCall(fn_name, arg_expr_list, def_val, pos) => { @@ -378,7 +448,7 @@ impl Engine<'_> { .map(|arg_expr| self.eval_expr(scope, arg_expr, level)) .collect::, _>>()?; - let this_ptr = get_this_ptr(scope, src, target); + let this_ptr = target.get_mut(scope); let mut arg_values: Vec<_> = once(this_ptr) .chain(values.iter_mut().map(Dynamic::as_mut)) @@ -389,29 +459,23 @@ impl Engine<'_> { // xxx.id 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) + let this_ptr = target.get_mut(scope); + self.call_fn_raw(&make_getter(id), &mut [this_ptr], None, *pos, 0) } // 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 value = 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, - ) + let this_ptr = target.get_mut(scope); + self.call_fn_raw(&make_getter(id), &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, target, idx_lhs, level)? + } // Syntax error _ => { return Err(EvalAltResult::ErrorDotExpr( @@ -421,41 +485,33 @@ 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, &value, idx_expr, *op_pos, level) + .map(|(val, _, _)| val) } // xxx.dot_lhs.rhs Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() { // xxx.id.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) - .and_then(|mut v| { - self.get_dot_val_helper(scope, None, Some(v.as_mut()), rhs, level) + let this_ptr = target.get_mut(scope); + self.call_fn_raw(&make_getter(id), &mut [this_ptr], None, *pos, 0) + .and_then(|mut val| { + self.get_dot_val_helper(scope, Target::from(val.as_mut()), rhs, level) }) } // 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, - ) + let this_ptr = target.get_mut(scope); + self.call_fn_raw(&make_getter(id), &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, target, idx_lhs, level)? + } // Syntax error _ => { return Err(EvalAltResult::ErrorDotExpr( @@ -465,10 +521,9 @@ 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_dot_val_helper(scope, None, Some(v.as_mut()), rhs, level) + self.get_indexed_value(scope, &val, idx_expr, *op_pos, level) + .and_then(|(mut val, _, _)| { + self.get_dot_val_helper(scope, Target::from(val.as_mut()), rhs, level) }) } // Syntax error @@ -487,6 +542,7 @@ impl Engine<'_> { } /// Evaluate a dot chain getter + #[cfg(not(feature = "no_object"))] fn get_dot_val( &mut self, scope: &mut Scope, @@ -497,23 +553,23 @@ 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 }; // This is a variable property access (potential function call). // Use a direct index into `scope` to directly mutate the variable value. - self.get_dot_val_helper(scope, Some(entry), None, dot_rhs, level) + self.get_dot_val_helper(scope, Target::from_src(entry), dot_rhs, level) } // 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 val) = 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); + let value = + self.get_dot_val_helper(scope, Target::from(val.as_mut()), dot_rhs, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some(src) = src { @@ -526,84 +582,101 @@ impl Engine<'_> { } ScopeEntryType::Normal => { Self::update_indexed_var_in_scope( - src_type, + idx_src_type, scope, src, idx, - (target, dot_rhs.position()), + (val, dot_rhs.position()), )?; } } } - val + value } // {expr}.??? expr => { - let mut target = self.eval_expr(scope, expr, level)?; - let this_ptr = target.as_mut(); - self.get_dot_val_helper(scope, None, Some(this_ptr), dot_rhs, level) + let mut val = self.eval_expr(scope, expr, level)?; + self.get_dot_val_helper(scope, Target::from(val.as_mut()), dot_rhs, level) } } } /// 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> { - if val.is::() { - // val_array[idx] - let arr = val.downcast_ref::().expect("array expected"); + level: usize, + ) -> Result<(Dynamic, IndexSourceType, IndexValue), EvalAltResult> { + let idx_pos = idx_expr.position(); - if idx >= 0 { + // val_array[idx] + if let Some(arr) = val.downcast_ref::() { + 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, IndexValue::from_num(idx))) .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos)) } else { Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos)) - } - } else if val.is::() { - // val_string[idx] - let s = val.downcast_ref::().expect("string expected"); + }; + } - if idx >= 0 { + #[cfg(not(feature = "no_object"))] + { + // val_map[idx] + if let Some(map) = val.downcast_ref::() { + 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, + IndexValue::from_str(idx), + )); + } + } + + // val_string[idx] + if let Some(s) = val.downcast_ref::() { + 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, + IndexValue::from_num(idx), + ) + }) .ok_or_else(|| { EvalAltResult::ErrorStringBounds(s.chars().count(), idx, idx_pos) }) @@ -613,14 +686,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 + Err(EvalAltResult::ErrorIndexingType( + self.map_type_name(val.type_name()).to_string(), + op_pos, + )) } /// Evaluate an index expression @@ -632,36 +705,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>, + IndexValue, + 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(|(val, _, idx)| (IndexSourceType::Expression, None, idx, val)) } } } @@ -683,17 +768,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: IndexValue, 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.as_num()] = 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.as_str(), new_val.0); Ok(().into_dynamic()) } @@ -706,7 +799,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.as_num(), ch); Ok(().into_dynamic()) } @@ -718,29 +811,38 @@ impl Engine<'_> { #[cfg(not(feature = "no_index"))] fn update_indexed_value( mut target: Dynamic, - idx: usize, + idx: IndexValue, 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::() { - let s = target.downcast_mut::().expect("string expected"); + if let Some(arr) = target.downcast_mut::() { + arr[idx.as_num()] = new_val; + return Ok(target); + } + + #[cfg(not(feature = "no_object"))] + { + if let Some(map) = target.downcast_mut::() { + map.insert(idx.as_str(), new_val); + return Ok(target); + } + } + + if let Some(s) = target.downcast_mut::() { // 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.as_num(), 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 + #[cfg(not(feature = "no_object"))] fn set_dot_val_helper( &mut self, scope: &mut Scope, @@ -752,9 +854,8 @@ impl Engine<'_> { match dot_rhs { // xxx.id Expr::Property(id, pos) => { - let set_fn_name = format!("{}{}", FUNC_SETTER, id); let mut args = [this_ptr, new_val.0.as_mut()]; - self.call_fn_raw(&set_fn_name, &mut args, None, *pos, 0) + self.call_fn_raw(&make_setter(id), &mut args, None, *pos, 0) } // xxx.lhs[idx_expr] @@ -762,25 +863,18 @@ impl Engine<'_> { #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() { // xxx.id[idx_expr] - Expr::Property(id, pos) => { - let get_fn_name = format!("{}{}", FUNC_GETTER, id); + Expr::Property(id, pos) => self + .call_fn_raw(&make_getter(id), &mut [this_ptr], None, *pos, 0) + .and_then(|val| { + let (_, _, idx) = + self.get_indexed_value(scope, &val, idx_expr, *op_pos, level)?; - 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, - ) - }) - .and_then(|mut v| { - let set_fn_name = format!("{}{}", FUNC_SETTER, id); - let mut args = [this_ptr, v.as_mut()]; - self.call_fn_raw(&set_fn_name, &mut args, None, *pos, 0) - }) - } + Self::update_indexed_value(val, idx, new_val.0.clone(), new_val.1) + }) + .and_then(|mut val| { + let mut args = [this_ptr, val.as_mut()]; + self.call_fn_raw(&make_setter(id), &mut args, None, *pos, 0) + }), // All others - syntax error for setters chain _ => Err(EvalAltResult::ErrorDotExpr( @@ -793,17 +887,14 @@ impl Engine<'_> { Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { // xxx.id.rhs Expr::Property(id, pos) => { - let get_fn_name = format!("{}{}", FUNC_GETTER, id); - - self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0) - .and_then(|mut v| { - self.set_dot_val_helper(scope, v.as_mut(), rhs, new_val, level) - .map(|_| v) // Discard Ok return value + self.call_fn_raw(&make_getter(id), &mut [this_ptr], None, *pos, 0) + .and_then(|mut val| { + self.set_dot_val_helper(scope, val.as_mut(), rhs, new_val, level) + .map(|_| val) // Discard Ok return value }) - .and_then(|mut v| { - let set_fn_name = format!("{}{}", FUNC_SETTER, id); - let mut args = [this_ptr, v.as_mut()]; - self.call_fn_raw(&set_fn_name, &mut args, None, *pos, 0) + .and_then(|mut val| { + let mut args = [this_ptr, val.as_mut()]; + self.call_fn_raw(&make_setter(id), &mut args, None, *pos, 0) }) } @@ -813,25 +904,26 @@ impl Engine<'_> { Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() { // xxx.id[idx_expr].rhs Expr::Property(id, pos) => { - let get_fn_name = format!("{}{}", FUNC_GETTER, id); - - self.call_fn_raw(&get_fn_name, &mut [this_ptr], None, *pos, 0) + self.call_fn_raw(&make_getter(id), &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 value, _, idx) = + self.get_indexed_value(scope, &v, idx_expr, *op_pos, level)?; let val_pos = new_val.1; - let this_ptr = target.as_mut(); + let this_ptr = value.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, value, val_pos) }) .and_then(|mut v| { - let set_fn_name = format!("{}{}", FUNC_SETTER, id); - let mut args = [this_ptr, v.as_mut()]; - self.call_fn_raw(&set_fn_name, &mut args, None, *pos, 0) + self.call_fn_raw( + &make_setter(id), + &mut [this_ptr, v.as_mut()], + None, + *pos, + 0, + ) }) } @@ -858,6 +950,7 @@ impl Engine<'_> { } // Evaluate a dot chain setter + #[cfg(not(feature = "no_object"))] fn set_dot_val( &mut self, scope: &mut Scope, @@ -870,7 +963,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( @@ -881,12 +974,13 @@ impl Engine<'_> { // Avoid referencing scope which is used below as mut let entry = ScopeSource { name: id, ..entry }; let this_ptr = target.as_mut(); - let val = self.set_dot_val_helper(scope, this_ptr, dot_rhs, new_val, level); + let value = + self.set_dot_val_helper(scope, this_ptr, dot_rhs, new_val, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. *scope.get_mut(entry) = target; - val + value } } } @@ -895,11 +989,11 @@ 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(); - let val = self.set_dot_val_helper(scope, this_ptr, dot_rhs, new_val, level); + let value = self.set_dot_val_helper(scope, this_ptr, dot_rhs, new_val, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some(src) = src { @@ -912,7 +1006,7 @@ impl Engine<'_> { } ScopeEntryType::Normal => { Self::update_indexed_var_in_scope( - src_type, + idx_src_type, scope, src, idx, @@ -922,7 +1016,7 @@ impl Engine<'_> { } } - val + value } // Syntax error @@ -947,7 +1041,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] @@ -994,7 +1088,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 { @@ -1006,7 +1100,7 @@ impl Engine<'_> { )) } ScopeEntryType::Normal => Ok(Self::update_indexed_var_in_scope( - src_type, + idx_src_type, scope, src, idx, @@ -1021,6 +1115,7 @@ impl Engine<'_> { } // dot_lhs.dot_rhs = rhs + #[cfg(not(feature = "no_object"))] Expr::Dot(dot_lhs, dot_rhs, _) => self.set_dot_val( scope, dot_lhs, @@ -1041,11 +1136,12 @@ impl Engine<'_> { } } + #[cfg(not(feature = "no_object"))] Expr::Dot(lhs, rhs, _) => self.get_dot_val(scope, lhs, rhs, level), #[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)) @@ -1054,12 +1150,25 @@ 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 { let spec = FnSpec { name: name.into(), - args: Some(vec![TypeId::of::()]), + args: vec![TypeId::of::()], }; engine.functions.contains_key(&spec) || engine.fn_lib.has_function(name, 1) @@ -1354,7 +1463,7 @@ impl Engine<'_> { pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { self.type_names .get(name) - .map(|s| s.as_str()) + .map(String::as_str) .unwrap_or(name) } diff --git a/src/error.rs b/src/error.rs index 4ec60ec9..2a2986b8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,32 +47,24 @@ pub enum ParseErrorType { UnexpectedEOF, /// An unknown operator is encountered. Wrapped value is the operator. UnknownOperator(String), - /// An open `(` is missing the corresponding closing `)`. - MissingRightParen(String), - /// Expecting `(` but not finding one. - MissingLeftBrace, - /// An open `{` is missing the corresponding closing `}`. - MissingRightBrace(String), - /// An open `[` is missing the corresponding closing `]`. - #[cfg(not(feature = "no_index"))] - MissingRightBracket(String), - /// A list of expressions is missing the separating ','. - MissingComma(String), - /// A statement is missing the ending ';'. - MissingSemicolon(String), + /// Expecting a particular token but not finding one. Wrapped values are the token and usage. + MissingToken(String, String), /// An expression in function call arguments `()` has syntax error. MalformedCallExpr(String), /// 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 custom types and maps. + PropertyExpected, /// Missing a variable name after the `let`, `const` or `for` keywords. VariableExpected, /// Missing an expression. ExprExpected(String), - /// A `for` statement is missing the `in` keyword. - MissingIn, /// Defining a function `fn` in an appropriate place (e.g. inside another function). #[cfg(not(feature = "no_function"))] WrongFnDefinition, @@ -84,7 +76,7 @@ pub enum ParseErrorType { FnMissingParams(String), /// A function definition has duplicated parameters. Wrapped values are the function name and parameter name. #[cfg(not(feature = "no_function"))] - FnDuplicateParam(String, String), + FnDuplicatedParam(String, String), /// A function definition is missing the body. Wrapped value is the function name. #[cfg(not(feature = "no_function"))] FnMissingBody(String), @@ -130,18 +122,14 @@ impl ParseError { ParseErrorType::BadInput(ref p) => p, ParseErrorType::UnexpectedEOF => "Script is incomplete", ParseErrorType::UnknownOperator(_) => "Unknown operator", - ParseErrorType::MissingRightParen(_) => "Expecting ')'", - ParseErrorType::MissingLeftBrace => "Expecting '{'", - ParseErrorType::MissingRightBrace(_) => "Expecting '}'", - #[cfg(not(feature = "no_index"))] - ParseErrorType::MissingRightBracket(_) => "Expecting ']'", - ParseErrorType::MissingComma(_) => "Expecting ','", - ParseErrorType::MissingSemicolon(_) => "Expecting ';'", + ParseErrorType::MissingToken(_, _) => "Expecting a certain token that is missing", 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::MissingIn => "Expecting 'in'", + ParseErrorType::PropertyExpected => "Expecting name of a property", ParseErrorType::VariableExpected => "Expecting name of a variable", ParseErrorType::ExprExpected(_) => "Expecting an expression", #[cfg(not(feature = "no_function"))] @@ -149,7 +137,7 @@ impl ParseError { #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", #[cfg(not(feature = "no_function"))] - ParseErrorType::FnDuplicateParam(_,_) => "Duplicated parameters in function declaration", + ParseErrorType::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration", #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration", #[cfg(not(feature = "no_function"))] @@ -180,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"))] @@ -193,19 +186,12 @@ impl fmt::Display for ParseError { } #[cfg(not(feature = "no_function"))] - ParseErrorType::FnDuplicateParam(ref s, ref arg) => { + ParseErrorType::FnDuplicatedParam(ref s, ref arg) => { write!(f, "Duplicated parameter '{}' for function '{}'", arg, s)? } - ParseErrorType::MissingRightParen(ref s) | ParseErrorType::MissingRightBrace(ref s) => { - write!(f, "{} for {}", self.desc(), s)? - } - - #[cfg(not(feature = "no_index"))] - ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, - - ParseErrorType::MissingSemicolon(ref s) | ParseErrorType::MissingComma(ref s) => { - write!(f, "{} for {}", self.desc(), s)? + ParseErrorType::MissingToken(ref token, ref s) => { + write!(f, "Expecting '{}' {}", token, s)? } ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { diff --git a/src/fn_register.rs b/src/fn_register.rs index df2d01d0..cfc1b345 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -145,7 +145,7 @@ macro_rules! def_register { fn register_fn(&mut self, name: &str, f: FN) { let fn_name = name.to_string(); - let fun = move |args: &mut FnCallArgs, pos: Position| { + let func = move |args: &mut FnCallArgs, pos: Position| { // Check for length at the beginning to avoid per-element bound checks. const NUM_ARGS: usize = count_args!($($par)*); @@ -165,7 +165,7 @@ macro_rules! def_register { let r = f($(($clone)($par)),*); Ok(Box::new(r) as Dynamic) }; - self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun)); + self.register_fn_raw(name, vec![$(TypeId::of::<$par>()),*], Box::new(func)); } } @@ -177,7 +177,7 @@ macro_rules! def_register { fn register_dynamic_fn(&mut self, name: &str, f: FN) { let fn_name = name.to_string(); - let fun = move |args: &mut FnCallArgs, pos: Position| { + let func = move |args: &mut FnCallArgs, pos: Position| { // Check for length at the beginning to avoid per-element bound checks. const NUM_ARGS: usize = count_args!($($par)*); @@ -196,7 +196,7 @@ macro_rules! def_register { // potentially clone the value, otherwise pass the reference. Ok(f($(($clone)($par)),*)) }; - self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun)); + self.register_fn_raw(name, vec![$(TypeId::of::<$par>()),*], Box::new(func)); } } @@ -209,7 +209,7 @@ macro_rules! def_register { fn register_result_fn(&mut self, name: &str, f: FN) { let fn_name = name.to_string(); - let fun = move |args: &mut FnCallArgs, pos: Position| { + let func = move |args: &mut FnCallArgs, pos: Position| { // Check for length at the beginning to avoid per-element bound checks. const NUM_ARGS: usize = count_args!($($par)*); @@ -229,7 +229,7 @@ macro_rules! def_register { f($(($clone)($par)),*).map(|r| Box::new(r) as Dynamic) .map_err(|err| err.set_position(pos)) }; - self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun)); + self.register_fn_raw(name, vec![$(TypeId::of::<$par>()),*], Box::new(func)); } } 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 c8dc32f7..0651536c 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -37,7 +37,15 @@ struct State<'a> { engine: &'a Engine<'a>, } -impl State<'_> { +impl<'a> State<'a> { + /// Create a new State. + pub fn new(engine: &'a Engine<'a>) -> Self { + Self { + changed: false, + constants: vec![], + engine, + } + } /// Reset the state from dirty to clean. pub fn reset(&mut self) { self.changed = false; @@ -334,6 +342,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { expr => Expr::Assignment(id, Box::new(optimize_expr(expr, state)), pos), }, // lhs.rhs + #[cfg(not(feature = "no_object"))] Expr::Dot(lhs, rhs, pos) => Expr::Dot( Box::new(optimize_expr(*lhs, state)), Box::new(optimize_expr(*rhs, state)), @@ -369,20 +378,16 @@ 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 + Expr::Array(items, pos) => Expr::Array(items .into_iter() .map(|expr| optimize_expr(expr, state)) - .collect(); - - if orig_len != items.len() { - state.set_dirty(); - } - - Expr::Array(items, pos) - } + .collect(), pos), + // [ items .. ] + #[cfg(not(feature = "no_object"))] + Expr::Map(items, pos) => Expr::Map(items + .into_iter() + .map(|(key, expr, pos)| (key, optimize_expr(expr, state), pos)) + .collect(), pos), // lhs && rhs Expr::And(lhs, rhs) => match (*lhs, *rhs) { // true && rhs -> rhs @@ -434,7 +439,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST => Expr::FunctionCall(id, args, def_value, pos), - // Do not optimize anything within built-in function keywords + // Do not call some special keywords Expr::FunctionCall(id, args, def_value, pos) if DONT_EVAL_KEYWORDS.contains(&id.as_str())=> Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), @@ -460,8 +465,8 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { "" }; - state.engine.call_ext_fn_raw(&id, &mut call_args, pos).ok().map(|r| - r.or_else(|| { + state.engine.call_ext_fn_raw(&id, &mut call_args, pos).ok().map(|result| + result.or_else(|| { if !arg_for_type_of.is_empty() { // Handle `type_of()` Some(arg_for_type_of.to_string().into_dynamic()) @@ -501,11 +506,7 @@ pub(crate) fn optimize<'a>(statements: Vec, engine: &Engine<'a>, scope: &S } // Set up the state - let mut state = State { - changed: false, - constants: vec![], - engine, - }; + let mut state = State::new(engine); // Add constants from the scope into the state scope diff --git a/src/parser.rs b/src/parser.rs index 5f53e3e7..f44660c4 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, @@ -177,12 +179,14 @@ impl AST { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// # #[cfg(not(feature = "no_function"))] + /// # { /// use rhai::Engine; /// /// let mut engine = Engine::new(); /// /// let ast1 = engine.compile(r#"fn foo(x) { 42 + x } foo(1)"#)?; - /// let ast2 = engine.compile(r#"fn foo(n) { "hello" + n } foo(2)"#)?; + /// let ast2 = engine.compile(r#"fn foo(n) { "hello" + n } foo("!")"#)?; /// /// let ast = ast1.merge(ast2); // Merge 'ast2' into 'ast1' /// @@ -194,10 +198,11 @@ impl AST { /// // fn foo(n) { "hello" + n } // <- definition of first 'foo' is overwritten /// // foo(1) // <- notice this will be "hello1" instead of 43, /// // // but it is no longer the return value - /// // foo(2) // returns "hello2" + /// // foo("!") // returns "hello!" /// /// // Evaluate it - /// assert_eq!(engine.eval_ast::(&ast)?, "hello2"); + /// assert_eq!(engine.eval_ast::(&ast)?, "hello!"); + /// # } /// # Ok(()) /// # } /// ``` @@ -357,6 +362,7 @@ pub enum Expr { /// expr = expr Assignment(Box, Box, Position), /// lhs.rhs + #[cfg(not(feature = "no_object"))] Dot(Box, Box, Position), /// expr[expr] #[cfg(not(feature = "no_index"))] @@ -364,6 +370,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 @@ -398,6 +407,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(), @@ -443,10 +459,12 @@ impl Expr { | Expr::False(pos) | Expr::Unit(pos) => *pos, - Expr::Assignment(expr, _, _) - | Expr::Dot(expr, _, _) - | Expr::And(expr, _) - | Expr::Or(expr, _) => expr.position(), + Expr::Assignment(expr, _, _) | Expr::And(expr, _) | Expr::Or(expr, _) => { + expr.position() + } + + #[cfg(not(feature = "no_object"))] + Expr::Dot(expr, _, _) => expr.position(), #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => *pos, @@ -454,6 +472,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(), } @@ -531,6 +552,8 @@ pub enum Token { Colon, Comma, Period, + #[cfg(not(feature = "no_object"))] + MapStart, Equals, True, False, @@ -606,6 +629,8 @@ impl Token { Colon => ":", Comma => ",", Period => ".", + #[cfg(not(feature = "no_object"))] + MapStart => "#{", Equals => "=", True => "true", False => "false", @@ -794,6 +819,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(); @@ -933,20 +963,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, } @@ -957,8 +984,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' => [ @@ -989,8 +1015,7 @@ impl<'a> TokenIterator<'a> { } result.push(next_char_in_hex); - self.stream.next(); - self.advance(); + self.eat_next(); } } @@ -1044,8 +1069,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, } @@ -1136,10 +1160,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)), @@ -1148,24 +1178,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' { @@ -1179,8 +1206,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(); @@ -1209,8 +1235,7 @@ impl<'a> TokenIterator<'a> { } ('/', '=') => { - self.stream.next(); - self.advance(); + self.eat_next(); return Some((Token::DivideAssign, pos)); } ('/', _) => return Some((Token::Divide, pos)), @@ -1221,25 +1246,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 @@ -1250,18 +1271,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 @@ -1272,53 +1290,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)), @@ -1370,13 +1380,16 @@ fn parse_paren_expr<'a>( // ( xxx ) Some((Token::RightParen, _)) => Ok(expr), // ( xxx ??? - Some((_, pos)) => { - Err(PERR::MissingRightParen("a matching ( in the expression".into()).into_err(pos)) - } + Some((_, pos)) => Err(PERR::MissingToken( + ")".into(), + "for a matching ( in this expression".into(), + ) + .into_err(pos)), // ( xxx - None => { - Err(PERR::MissingRightParen("a matching ( in the expression".into()).into_err_eof()) - } + None => Err( + PERR::MissingToken(")".into(), "for a matching ( in this expression".into()) + .into_err_eof(), + ), } } @@ -1391,10 +1404,10 @@ fn parse_call_expr<'a>( // id() if let (Token::RightParen, _) = input.peek().ok_or_else(|| { - PERR::MissingRightParen(format!( - "closing the arguments to call of function '{}'", - id - )) + PERR::MissingToken( + ")".into(), + format!("to close the arguments list of this function call '{}'", id), + ) .into_err_eof() })? { input.next(); @@ -1405,10 +1418,10 @@ fn parse_call_expr<'a>( args_expr_list.push(parse_expr(input, allow_stmt_expr)?); match input.peek().ok_or_else(|| { - PERR::MissingRightParen(format!( - "closing the arguments to call of function '{}'", - id - )) + PERR::MissingToken( + ")".into(), + format!("to close the arguments list of this function call '{}'", id), + ) .into_err_eof() })? { (Token::RightParen, _) => { @@ -1417,10 +1430,10 @@ fn parse_call_expr<'a>( } (Token::Comma, _) => (), (_, pos) => { - return Err(PERR::MissingComma(format!( - "separating the arguments to call of function '{}'", - id - )) + return Err(PERR::MissingToken( + ",".into(), + format!("to separate the arguments to function call '{}'", id), + ) .into_err(*pos)) } } @@ -1429,7 +1442,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, @@ -1439,7 +1452,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 => { @@ -1449,6 +1462,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) => { @@ -1464,13 +1543,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( @@ -1497,15 +1569,22 @@ fn parse_index_expr<'a>( } // Check if there is a closing bracket - match input - .peek() - .ok_or_else(|| PERR::MissingRightBracket("index expression".into()).into_err_eof())? - { + match input.peek().ok_or_else(|| { + PERR::MissingToken( + "]".into(), + "for a matching [ in this index expression".into(), + ) + .into_err_eof() + })? { (Token::RightBracket, _) => { input.next(); Ok(Expr::Index(lhs, Box::new(idx_expr), pos)) } - (_, pos) => Err(PERR::MissingRightBracket("index expression".into()).into_err(*pos)), + (_, pos) => Err(PERR::MissingToken( + "]".into(), + "for a matching [ in this index expression".into(), + ) + .into_err(*pos)), } } @@ -1555,35 +1634,141 @@ fn parse_array_literal<'a>( arr.push(parse_expr(input, allow_stmt_expr)?); match input.peek().ok_or_else(|| { - PERR::MissingRightBracket("separating items in array literal".into()).into_err_eof() + PERR::MissingToken("]".into(), "to end this array literal".into()).into_err_eof() })? { (Token::Comma, _) => { input.next(); } (Token::RightBracket, _) => break, (_, pos) => { - return Err( - PERR::MissingComma("separating items in array literal".into()) - .into_err(*pos), + return Err(PERR::MissingToken( + ",".into(), + "to separate the items of this array literal".into(), ) + .into_err(*pos)) } } } } match input.peek().ok_or_else(|| { - PERR::MissingRightBracket("the end of array literal".into()).into_err_eof() + PERR::MissingToken("]".into(), "to end this array literal".into()).into_err_eof() })? { (Token::RightBracket, _) => { input.next(); Ok(Expr::Array(arr, begin)) } (_, pos) => { - Err(PERR::MissingRightBracket("the end of array literal".into()).into_err(*pos)) + Err(PERR::MissingToken("]".into(), "to end this array literal".into()).into_err(*pos)) } } } +/// 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), + (Token::StringConst(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>, @@ -1627,6 +1812,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)), @@ -1756,6 +1946,7 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result match dot_lhs.as_ref() { // var.dot_rhs Expr::Variable(_, _) if is_top => valid_assignment_chain(dot_rhs, false), @@ -1864,28 +2055,32 @@ fn parse_binary_op<'a>( Token::PlusAssign => parse_op_assignment("+", current_lhs, rhs, pos)?, Token::MinusAssign => parse_op_assignment("-", current_lhs, rhs, pos)?, + #[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 @@ -2070,9 +2265,16 @@ fn parse_for<'a>( }; // for name in ... - match input.next().ok_or_else(|| PERR::MissingIn.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::MissingIn.into_err(pos)), + (_, pos) => { + return Err( + PERR::MissingToken("in".into(), "after the iteration variable".into()) + .into_err(pos), + ) + } } // for name in expr { body } @@ -2136,10 +2338,14 @@ fn parse_block<'a>( // Must start with { let pos = match input .next() - .ok_or_else(|| PERR::MissingLeftBrace.into_err_eof())? + .ok_or_else(|| PERR::UnexpectedEOF.into_err_eof())? { (Token::LeftBrace, pos) => pos, - (_, pos) => return Err(PERR::MissingLeftBrace.into_err(pos)), + (_, pos) => { + return Err( + PERR::MissingToken("{".into(), "to start a statement block".into()).into_err(pos), + ) + } }; let mut statements = Vec::new(); @@ -2169,20 +2375,24 @@ fn parse_block<'a>( // { ... stmt ??? - error Some((_, pos)) => { // Semicolons are not optional between statements - return Err(PERR::MissingSemicolon("terminating a statement".into()).into_err(*pos)); + return Err( + PERR::MissingToken(";".into(), "to terminate this statement".into()) + .into_err(*pos), + ); } } } - match input - .peek() - .ok_or_else(|| PERR::MissingRightBrace("end of block".into()).into_err_eof())? - { + match input.peek().ok_or_else(|| { + PERR::MissingToken("}".into(), "to end this statement block".into()).into_err_eof() + })? { (Token::RightBrace, _) => { input.next(); Ok(Stmt::Block(statements, pos)) } - (_, pos) => Err(PERR::MissingRightBrace("end of block".into()).into_err(*pos)), + (_, pos) => { + Err(PERR::MissingToken("}".into(), "to end this statement block".into()).into_err(*pos)) + } } } @@ -2284,28 +2494,30 @@ fn parse_fn<'a>( if matches!(input.peek(), Some((Token::RightParen, _))) { input.next(); } else { - let end_err = format!("closing the parameters list of function '{}'", name); - let sep_err = format!("separating the parameters of function '{}'", name); + let end_err = format!("to close the parameters list of function '{}'", name); + let sep_err = format!("to separate the parameters of function '{}'", name); loop { match input .next() - .ok_or_else(|| PERR::MissingRightParen(end_err.to_string()).into_err_eof())? + .ok_or_else(|| PERR::MissingToken(")".into(), end_err.to_string()).into_err_eof())? { (Token::Identifier(s), pos) => { params.push((s, pos)); } - (_, pos) => return Err(PERR::MissingRightParen(end_err).into_err(pos)), + (_, pos) => return Err(PERR::MissingToken(")".into(), end_err).into_err(pos)), } match input .next() - .ok_or_else(|| PERR::MissingRightParen(end_err.to_string()).into_err_eof())? + .ok_or_else(|| PERR::MissingToken(")".into(), end_err.to_string()).into_err_eof())? { (Token::RightParen, _) => break, (Token::Comma, _) => (), - (Token::Identifier(_), _) => return Err(PERR::MissingComma(sep_err).into_err(pos)), - (_, pos) => return Err(PERR::MissingRightParen(sep_err).into_err(pos)), + (Token::Identifier(_), pos) => { + return Err(PERR::MissingToken(",".into(), sep_err).into_err(pos)) + } + (_, pos) => return Err(PERR::MissingToken(",".into(), sep_err).into_err(pos)), } } } @@ -2322,7 +2534,7 @@ fn parse_fn<'a>( .map_or_else(|| Ok(()), |(p2, pos)| Err((p2, *pos))) }) .map_err(|(p, pos)| { - PERR::FnDuplicateParam(name.to_string(), p.to_string()).into_err(pos) + PERR::FnDuplicatedParam(name.to_string(), p.to_string()).into_err(pos) })?; // Parse function body @@ -2408,7 +2620,10 @@ fn parse_global_level<'a>( // stmt ??? - error Some((_, pos)) => { // Semicolons are not optional between statements - return Err(PERR::MissingSemicolon("terminating a statement".into()).into_err(*pos)); + return Err( + PERR::MissingToken(";".into(), "to terminate this statement".into()) + .into_err(*pos), + ); } } } diff --git a/src/result.rs b/src/result.rs index e96a29c7..0c882609 100644 --- a/src/result.rs +++ b/src/result.rs @@ -20,6 +20,11 @@ use crate::stdlib::path::PathBuf; pub enum EvalAltResult { /// Syntax error. ErrorParsing(ParseError), + + /// Error reading from a script file. Wrapped value is the path of the script file. + #[cfg(not(feature = "no_std"))] + ErrorReadingScriptFile(PathBuf, std::io::Error), + /// Call to an unknown function. Wrapped value is the name of the function. ErrorFunctionNotFound(String, Position), /// Function call has incorrect number of arguments. @@ -36,10 +41,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. @@ -53,9 +60,6 @@ pub enum EvalAltResult { /// Returned type is not the same as the required output type. /// Wrapped value is the type of the actual result. ErrorMismatchOutputType(String, Position), - /// Error reading from a script file. Wrapped value is the path of the script file. - #[cfg(not(feature = "no_std"))] - ErrorReadingScriptFile(PathBuf, std::io::Error), /// Inappropriate member access. ErrorDotExpr(String, Position), /// Arithmetic error encountered. Wrapped value is the error message. @@ -64,6 +68,7 @@ pub enum EvalAltResult { ErrorStackOverflow(Position), /// Run-time error encountered. Wrapped value is the error message. ErrorRuntime(String, Position), + /// Breaking out of loops - not an error if within a loop. ErrorLoopBreak(Position), /// Not an error: Value returned from a script via the `return` keyword. @@ -74,6 +79,9 @@ pub enum EvalAltResult { impl EvalAltResult { pub(crate) fn desc(&self) -> &str { match self { + #[cfg(not(feature = "no_std"))] + Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file", + Self::ErrorParsing(p) => p.desc(), Self::ErrorFunctionNotFound(_, _) => "Function not found", Self::ErrorFunctionArgsMismatch(_, _, _, _) => { @@ -81,9 +89,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" @@ -103,8 +114,6 @@ impl EvalAltResult { } Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable", Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect", - #[cfg(not(feature = "no_std"))] - Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file", Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorStackOverflow(_) => "Stack overflow", @@ -122,43 +131,52 @@ 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::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::ErrorLoopBreak(pos) => write!(f, "{} ({})", desc, pos), - Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), #[cfg(not(feature = "no_std"))] Self::ErrorReadingScriptFile(path, err) => { write!(f, "{} '{}': {}", desc, path.display(), err) } + Self::ErrorParsing(p) => write!(f, "Syntax error: {}", p), - Self::ErrorFunctionArgsMismatch(fun, 0, n, pos) => write!( + + 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::ErrorRuntime(s, pos) => { + write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) + } + + Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), + Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), + Self::ErrorArithmetic(s, pos) => write!(f, "{} ({})", s, pos), + + Self::ErrorLoopBreak(pos) => write!(f, "{} ({})", desc, pos), + Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), + + Self::ErrorFunctionArgsMismatch(fn_name, 0, n, pos) => write!( f, "Function '{}' expects no argument but {} found ({})", - fun, n, pos + fn_name, n, pos ), - Self::ErrorFunctionArgsMismatch(fun, 1, n, pos) => write!( + Self::ErrorFunctionArgsMismatch(fn_name, 1, n, pos) => write!( f, "Function '{}' expects one argument but {} found ({})", - fun, n, pos + fn_name, n, pos ), - Self::ErrorFunctionArgsMismatch(fun, need, n, pos) => write!( + Self::ErrorFunctionArgsMismatch(fn_name, need, n, pos) => write!( f, "Function '{}' expects {} argument(s) but {} found ({})", - fun, need, n, pos + fn_name, need, n, pos ), Self::ErrorBooleanArgMismatch(op, pos) => { write!(f, "{} operator expects boolean operands ({})", op, pos) @@ -225,7 +243,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 +275,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/arrays.rs b/tests/arrays.rs index 710d0cf7..5cfec145 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -16,6 +16,7 @@ fn test_arrays() -> Result<(), EvalAltResult> { } #[test] +#[cfg(not(feature = "no_object"))] fn test_array_with_structs() -> Result<(), EvalAltResult> { #[derive(Clone)] struct TestStruct { diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 39131fea..c5b8b307 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -11,7 +11,7 @@ fn test_fn() -> Result<(), EvalAltResult> { .expect_err("should be error") .error_type() { - ParseErrorType::FnDuplicateParam(f, p) if f == "hello" && p == "x" => (), + ParseErrorType::FnDuplicatedParam(f, p) if f == "hello" && p == "x" => (), _ => assert!(false, "wrong error"), } diff --git a/tests/chars.rs b/tests/chars.rs index 340cc392..2739401b 100644 --- a/tests/chars.rs +++ b/tests/chars.rs @@ -12,7 +12,7 @@ fn test_chars() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::(r#"let x="hello"; x[2]"#)?, 'l'); assert_eq!( engine.eval::(r#"let y="hello"; y[2]='$'; y"#)?, - "he$lo".to_string() + "he$lo" ); } diff --git a/tests/expressions.rs b/tests/expressions.rs index 3d42ed98..9023f97c 100644 --- a/tests/expressions.rs +++ b/tests/expressions.rs @@ -27,6 +27,7 @@ fn test_expressions() -> Result<(), EvalAltResult> { /// This example taken from https://github.com/jonathandturner/rhai/issues/115 #[test] +#[cfg(not(feature = "no_object"))] fn test_expressions_eval() -> Result<(), EvalAltResult> { #[derive(Debug, Clone)] struct AGENT { diff --git a/tests/float.rs b/tests/float.rs index 688c095f..20349098 100644 --- a/tests/float.rs +++ b/tests/float.rs @@ -21,6 +21,7 @@ fn test_float() -> Result<(), EvalAltResult> { } #[test] +#[cfg(not(feature = "no_object"))] fn struct_with_float() -> Result<(), EvalAltResult> { #[derive(Clone)] struct TestStruct { diff --git a/tests/get_set.rs b/tests/get_set.rs index a1c64d25..9f63a365 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -1,3 +1,5 @@ +#![cfg(not(feature = "no_object"))] + use rhai::{Engine, EvalAltResult, RegisterFn, INT}; #[test] diff --git a/tests/increment.rs b/tests/increment.rs index 980dda56..72c6eb35 100644 --- a/tests/increment.rs +++ b/tests/increment.rs @@ -7,7 +7,7 @@ fn test_increment() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::("let x = 1; x += 2; x")?, 3); assert_eq!( engine.eval::("let s = \"test\"; s += \"ing\"; s")?, - "testing".to_string() + "testing" ); Ok(()) diff --git a/tests/maps.rs b/tests/maps.rs new file mode 100644 index 00000000..0da9b589 --- /dev/null +++ b/tests/maps.rs @@ -0,0 +1,66 @@ +#![cfg(not(feature = "no_object"))] + +use rhai::{AnyExt, Engine, EvalAltResult, Map, INT}; + +#[test] +fn test_map_indexing() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + #[cfg(not(feature = "no_index"))] + 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 + ); + + #[cfg(not(feature = "no_index"))] + assert_eq!( + engine.eval::( + r#" + let y = #{d: 1, "e": #{a: 42, b: 88, "": "hello"}, " 123 xyz": 9}; + y.e[""][4] + "# + )?, + 'o' + ); + + 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 x = engine.eval::(r#"let x = #{a: 1, b: true, "c#": "hello"}; x"#)?; + let a = x.get("a").cloned().unwrap(); + let b = x.get("b").cloned().unwrap(); + let c = x.get("c#").cloned().unwrap(); + + assert_eq!(*a.downcast::().unwrap(), 1); + assert_eq!(*b.downcast::().unwrap(), true); + assert_eq!(*c.downcast::().unwrap(), "hello"); + + Ok(()) +} + +#[test] +fn test_map_return() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + let x = engine.eval::(r#"#{a: 1, b: true, c: "hello"}"#)?; + let a = x.get("a").cloned().unwrap(); + let b = x.get("b").cloned().unwrap(); + let c = x.get("c").cloned().unwrap(); + + assert_eq!(*a.downcast::().unwrap(), 1); + assert_eq!(*b.downcast::().unwrap(), true); + assert_eq!(*c.downcast::().unwrap(), "hello"); + + Ok(()) +} diff --git a/tests/math.rs b/tests/math.rs index 1bfdf521..24181c5f 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -12,12 +12,12 @@ fn test_math() -> Result<(), EvalAltResult> { #[cfg(not(feature = "only_i32"))] assert_eq!( - engine.eval::("(-9223372036854775807).abs()")?, + engine.eval::("abs(-9223372036854775807)")?, 9_223_372_036_854_775_807 ); #[cfg(feature = "only_i32")] - assert_eq!(engine.eval::("(-2147483647).abs()")?, 2147483647); + assert_eq!(engine.eval::("abs(-2147483647)")?, 2147483647); // Overflow/underflow/division-by-zero errors #[cfg(not(feature = "unchecked"))] @@ -26,7 +26,7 @@ fn test_math() -> Result<(), EvalAltResult> { { assert!(matches!( engine - .eval::("(-9223372036854775808).abs()") + .eval::("abs(-9223372036854775808)") .expect_err("expects negation overflow"), EvalAltResult::ErrorArithmetic(_, _) )); diff --git a/tests/method_call.rs b/tests/method_call.rs index 9e217306..a4e19251 100644 --- a/tests/method_call.rs +++ b/tests/method_call.rs @@ -1,8 +1,10 @@ +#![cfg(not(feature = "no_object"))] + use rhai::{Engine, EvalAltResult, RegisterFn, INT}; #[test] fn test_method_call() -> Result<(), EvalAltResult> { - #[derive(Clone)] + #[derive(Debug, Clone, Eq, PartialEq)] struct TestStruct { x: INT, } @@ -24,11 +26,15 @@ fn test_method_call() -> Result<(), EvalAltResult> { engine.register_fn("update", TestStruct::update); engine.register_fn("new_ts", TestStruct::new); - let ts = engine.eval::("let x = new_ts(); x.update(); x")?; - assert_eq!(ts.x, 1001); + assert_eq!( + engine.eval::("let x = new_ts(); x.update(); x")?, + TestStruct { x: 1001 } + ); - let ts = engine.eval::("let x = new_ts(); update(x); x")?; - assert_eq!(ts.x, 1); + assert_eq!( + engine.eval::("let x = new_ts(); update(x); x")?, + TestStruct { x: 1 } + ); Ok(()) } diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 3fcc02c6..5629f6cd 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -12,6 +12,7 @@ fn test_mismatched_op() { } #[test] +#[cfg(not(feature = "no_object"))] fn test_mismatched_op_custom_type() { #[derive(Clone)] struct TestStruct { diff --git a/tests/side_effects.rs b/tests/side_effects.rs index 69b82e64..54b25432 100644 --- a/tests/side_effects.rs +++ b/tests/side_effects.rs @@ -1,3 +1,5 @@ +#![cfg(not(feature = "no_object"))] + ///! This test simulates an external command object that is driven by a script. use rhai::{Engine, EvalAltResult, RegisterFn, Scope, INT}; use std::cell::RefCell; diff --git a/tests/string.rs b/tests/string.rs index 5d63b88f..92310e33 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -6,30 +6,24 @@ fn test_string() -> Result<(), EvalAltResult> { assert_eq!( engine.eval::(r#""Test string: \u2764""#)?, - "Test string: ❤".to_string() + "Test string: ❤" ); assert_eq!( engine.eval::(r#""Test string: \x58""#)?, - "Test string: X".to_string() + "Test string: X" ); - assert_eq!( - engine.eval::(r#""foo" + "bar""#)?, - "foobar".to_string() - ); + assert_eq!(engine.eval::(r#""foo" + "bar""#)?, "foobar"); #[cfg(not(feature = "no_stdlib"))] - assert_eq!( - engine.eval::(r#""foo" + 123"#)?, - "foo123".to_string() - ); + assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); #[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_stdlib"))] - assert_eq!( - engine.eval::(r#""foo" + 123.4556"#)?, - "foo123.4556".to_string() - ); + assert_eq!(engine.eval::(r#""foo" + 123.4556"#)?, "foo123.4556"); + + #[cfg(not(feature = "no_stdlib"))] + assert_eq!(engine.eval::("(42).to_string()")?, "42"); Ok(()) } diff --git a/tests/types.rs b/tests/types.rs index 0101a120..658d8b3f 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -1,7 +1,12 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, RegisterFn, INT}; #[test] fn test_type_of() -> Result<(), EvalAltResult> { + #[derive(Clone)] + struct TestStruct { + x: INT, + } + let mut engine = Engine::new(); #[cfg(not(feature = "only_i32"))] @@ -14,12 +19,25 @@ fn test_type_of() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::("type_of(1.0 + 2.0)")?, "f64"); #[cfg(not(feature = "no_index"))] - #[cfg(not(feature = "no_float"))] assert_eq!( - engine.eval::(r#"type_of([1.0, 2, "hello"])"#)?, + engine.eval::(r#"type_of([true, 2, "hello"])"#)?, "array" ); + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::(r#"type_of(#{a:true, "":2, "z":"hello"})"#)?, + "map" + ); + + #[cfg(not(feature = "no_object"))] + { + engine.register_type_with_name::("Hello"); + engine.register_fn("new_ts", || TestStruct { x: 1 }); + + assert_eq!(engine.eval::("type_of(new_ts())")?, "Hello"); + } + assert_eq!(engine.eval::(r#"type_of("hello")"#)?, "string"); #[cfg(not(feature = "only_i32"))]