diff --git a/Cargo.toml b/Cargo.toml index f586d1f3..fe543553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ include = [ "Cargo.toml" ] +[dependencies] +num-traits = "*" + [features] debug_msgs = [] -no_stdlib = [] \ No newline at end of file +no_stdlib = [] +unchecked = [] diff --git a/README.md b/README.md index c78534a0..71f33028 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Rhai - Embedded Scripting for Rust +Rhai - Embedded Scripting for Rust +================================= Rhai is an embedded scripting language for Rust that gives you a safe and easy way to add scripting to your applications. @@ -9,11 +10,12 @@ Rhai's current feature set: * Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app) * Easy-to-use language similar to JS+Rust * Support for overloaded functions -* No additional dependencies +* Very few additional dependencies (right now only `num-traits` to do checked arithmetic operations) **Note:** Currently, the version is 0.10.2, so the language and API's may change before they stabilize. -## Installation +Installation +------------ You can install Rhai using crates by adding this line to your dependencies: @@ -33,34 +35,37 @@ to use the latest version. Beware that in order to use pre-releases (alpha and beta) you need to specify the exact version in your `Cargo.toml`. -## Optional Features +Optional features +----------------- -### `debug_msgs` +| Feature | Description | +| ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `debug_msgs` | Print debug messages to stdout (using `println!`) related to function registrations and function calls. | +| `no_stdlib` | Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. | +| `unchecked` | Exclude arithmetic checking in the standard library. Beware that a bad script may panic the entire system! | -Print debug messages to stdout (using `println!`) related to function registrations and function calls. - -### `no_stdlib` - -Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. - -## Related +Related +------- Other cool projects to check out: * [ChaiScript](http://chaiscript.com/) - A strong inspiration for Rhai. An embedded scripting language for C++ that I helped created many moons ago, now being lead by my cousin. * You can also check out the list of [scripting languages for Rust](https://github.com/rust-unofficial/awesome-rust#scripting) on [awesome-rust](https://github.com/rust-unofficial/awesome-rust) -## Examples +Examples +-------- -The repository contains several examples in the `examples` folder: +A number of examples can be found in the `examples` folder: -* `arrays_and_structs` demonstrates registering a new type to Rhai and the usage of arrays on it -* `custom_types_and_methods` shows how to register a type and methods for it -* `hello` simple example that evaluates an expression and prints the result -* `reuse_scope` evaluates two pieces of code in separate runs, but using a common scope -* `rhai_runner` runs each filename passed to it as a Rhai script -* `simple_fn` shows how to register a Rust function to a Rhai engine -* `repl` a simple REPL, see source code for what it can do at the moment +| Example | Description | +| -------------------------- | ------------------------------------------------------------------------- | +| `arrays_and_structs` | demonstrates registering a new type to Rhai and the usage of arrays on it | +| `custom_types_and_methods` | shows how to register a type and methods for it | +| `hello` | simple example that evaluates an expression and prints the result | +| `reuse_scope` | evaluates two pieces of code in separate runs, but using a common scope | +| `rhai_runner` | runs each filename passed to it as a Rhai script | +| `simple_fn` | shows how to register a Rust function to a Rhai engine | +| `repl` | a simple REPL, interactively evaluate statements from stdin | Examples can be run with the following command: @@ -68,72 +73,87 @@ Examples can be run with the following command: cargo run --example name ``` -## Example Scripts +The `repl` example is a particularly good one as it allows you to interactively try out Rhai's +language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). -We also have a few examples scripts that showcase Rhai's features, all stored in the `scripts` folder: +Example Scripts +--------------- -* `array.rhai` - arrays in Rhai -* `assignment.rhai` - variable declarations -* `comments.rhai` - just comments -* `for1.rhai` - for loops -* `function_decl1.rhai` - a function without parameters -* `function_decl2.rhai` - a function with two parameters -* `function_decl3.rhai` - a function with many parameters -* `if1.rhai` - if example -* `loop.rhai` - endless loop in Rhai, this example emulates a do..while cycle -* `op1.rhai` - just a simple addition -* `op2.rhai` - simple addition and multiplication -* `op3.rhai` - change evaluation order with parenthesis -* `speed_test.rhai` - a simple program to measure the speed of Rhai's interpreter -* `string.rhai`- string operations -* `while.rhai` - while loop +There are also a number of examples scripts that showcase Rhai's features, all in the `scripts` folder: -To run the scripts, you can either make your own tiny program, or make use of the `rhai_runner` -example program: +| Script | Description | +| --------------------- | ------------------------------------------------------------- | +| `array.rhai` | arrays in Rhai | +| `assignment.rhai` | variable declarations | +| `comments.rhai` | just comments | +| `for1.rhai` | for loops | +| `function_decl1.rhai` | a function without parameters | +| `function_decl2.rhai` | a function with two parameters | +| `function_decl3.rhai` | a function with many parameters | +| `if1.rhai` | if example | +| `loop.rhai` | endless loop in Rhai, this example emulates a do..while cycle | +| `op1.rhai` | just a simple addition | +| `op2.rhai` | simple addition and multiplication | +| `op3.rhai` | change evaluation order with parenthesis | +| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter | +| `string.rhai` | string operations | +| `while.rhai` | while loop | + +To run the scripts, either make a tiny program or use of the `rhai_runner` example: ```bash cargo run --example rhai_runner scripts/any_script.rhai ``` -# Hello world +Hello world +----------- -To get going with Rhai, you create an instance of the scripting engine and then run eval. +To get going with Rhai, create an instance of the scripting engine and then call `eval`: ```rust -use rhai::Engine; +use rhai::{Engine, EvalAltResult}; -fn main() { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); - if let Ok(result) = engine.eval::("40 + 2") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("40 + 2")?; + + println!("Answer: {}", result); // prints 42 } ``` You can also evaluate a script file: ```rust -if let Ok(result) = engine.eval_file::("hello_world.rhai") { ... } +let result = engine.eval_file::("hello_world.rhai")?; ``` If you want to repeatedly evaluate a script, you can _compile_ it first into an AST (abstract syntax tree) form: ```rust +use rhai::Engine; + +let mut engine = Engine::new(); + // Compile to an AST and store it for later evaluations -let ast = Engine::compile("40 + 2").unwrap(); +let ast = engine.compile("40 + 2")?; for _ in 0..42 { - if let Ok(result) = engine.eval_ast::(&ast) { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval_ast::(&ast)?; + + println!("Answer: {}", result); // prints 42 } ``` Compiling a script file is also supported: ```rust -let ast = Engine::compile_file("hello_world.rhai").unwrap(); +use rhai::Engine; + +let mut engine = Engine::new(); + +let ast = engine.compile_file("hello_world.rhai").unwrap(); ``` Rhai also allows you to work _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust. @@ -141,26 +161,34 @@ You do this via `call_fn`, which takes a compiled AST (output from `compile`) an function call arguments: ```rust +use rhai::Engine; + +let mut engine = Engine::new(); + // Define a function in a script and compile to AST -let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; +let ast = engine.compile("fn hello(x, y) { x.len() + y }")?; // Evaluate the function in the AST, passing arguments into the script as a tuple // (beware, arguments must be of the correct types because Rhai does not have built-in type conversions) -let result: i64 = engine.call_fn("hello", ast, (&mut String::from("abc"), &mut 123_i64))?; +let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut 123_i64))?; ``` -# Values and types +Values and types +---------------- The following primitive types are supported natively: -* Integer: `i32`, `u32`, `i64` (default), `u64` -* Floating-point: `f32`, `f64` (default) -* Character: `char` -* Boolean: `bool` -* Array: `rhai::Array` -* Dynamic (i.e. can be anything): `rhai::Dynamic` +| Category | Types | +| ------------------------------ | -------------------------------------- | +| Integer | `i32`, `u32`, `i64` _(default)_, `u64` | +| Floating-point | `f32`, `f64` _(default)_ | +| Character | `char` | +| Boolean | `bool` | +| Array | `rhai::Array` | +| Dynamic (i.e. can be anything) | `rhai::Dynamic` | -# Value conversions +Value conversions +----------------- All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different; you cannot even add them together. @@ -187,12 +215,15 @@ if z.type_of() == "string" { } ``` -# Working with functions +Working with functions +---------------------- Rhai's scripting engine is very lightweight. It gets its ability from the functions in your program. To call these functions, you need to register them with the scripting engine. ```rust -use rhai::{Dynamic, Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult}; +use rhai::RegisterFn; // include the `RegisterFn` trait to use `register_fn` +use rhai::{Dynamic, RegisterDynamicFn}; // include the `RegisterDynamicFn` trait to use `register_dynamic_fn` // Normal function fn add(x: i64, y: i64) -> i64 { @@ -204,21 +235,24 @@ fn get_an_any() -> Dynamic { Box::new(42_i64) } -fn main() { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); engine.register_fn("add", add); - if let Ok(result) = engine.eval::("add(40, 2)") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("add(40, 2)")?; + + 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); - if let Ok(result) = engine.eval::("get_an_any()") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("get_an_any()")?; + + println!("Answer: {}", result); // prints 42 + + Ok(()) } ``` @@ -234,7 +268,8 @@ fn decide(yes_no: bool) -> Dynamic { } ``` -# Working with generic functions +Generic functions +----------------- Generic functions can be used in Rhai, but you'll need to register separate instances for each concrete type: @@ -247,7 +282,8 @@ fn showit(x: &mut T) -> () { println!("{}", x) } -fn main() { +fn main() +{ let mut engine = Engine::new(); engine.register_fn("print", showit as fn(x: &mut i64)->()); @@ -258,7 +294,42 @@ fn main() { You can also see in this example how you can register multiple functions (or in this case multiple instances of the same function) to the same name in script. This gives you a way to overload functions and call the correct one, based on the types of the arguments, from your script. -# Override built-in functions +Fallible functions +------------------ + +If your function is _fallible_ (i.e. it returns a `Result<_, Error>`), you can register it with `register_result_fn` (using the `RegisterResultFn` trait). + +Your function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements `From<&str>` and `From` etc. and the error text gets converted into `EvalAltResult::ErrorRuntime`. + +```rust +use rhai::{Engine, EvalAltResult, Position}; +use rhai::RegisterResultFn; // include the `RegisterResultFn` trait to use `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 + } else { + Ok(x / y) + } +} + +fn main() +{ + let mut engine = Engine::new(); + + // Fallible functions that return Result values must use register_result_fn() + 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)") + } +} +``` + +Overriding built-in functions +---------------------------- Any similarly-named function defined in a script overrides any built-in function. @@ -271,12 +342,14 @@ fn to_int(num) { print(to_int(123)); // what will happen? ``` -# Custom types and methods +Custom types and methods +----------------------- Here's an more complete example of working with Rust. First the example, then we'll break it into parts: ```rust -use rhai::{Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult}; +use rhai::RegisterFn; #[derive(Clone)] struct TestStruct { @@ -293,7 +366,8 @@ impl TestStruct { } } -fn main() { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); engine.register_type::(); @@ -301,9 +375,11 @@ fn main() { engine.register_fn("update", TestStruct::update); engine.register_fn("new_ts", TestStruct::new); - if let Ok(result) = engine.eval::("let x = new_ts(); x.update(); x") { - println!("result: {}", result.x); // prints 1001 - } + let result = engine.eval::("let x = new_ts(); x.update(); x")?; + + println!("result: {}", result.x); // prints 1001 + + Ok(()) } ``` @@ -346,9 +422,9 @@ engine.register_fn("new_ts", TestStruct::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. ```rust -if let Ok(result) = engine.eval::("let x = new_ts(); x.update(); x") { - println!("result: {}", result.x); // prints 1001 -} +let result = engine.eval::("let x = new_ts(); x.update(); x")?; + +println!("result: {}", result.x); // prints 1001 ``` 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. @@ -360,9 +436,9 @@ fn foo(ts: &mut TestStruct) -> i64 { engine.register_fn("foo", foo); -if let Ok(result) = engine.eval::("let x = new_ts(); x.foo()") { - println!("result: {}", result); // prints 1 -} +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: @@ -374,7 +450,8 @@ print(x.type_of()); // prints "foo::bar::TestStruct" If you use `register_type_with_name` to register the custom type with a special pretty-print name, `type_of` will return that instead. -# Getters and setters +Getters and setters +------------------- Similarly, you can work with members of your custom types. This works by registering a 'get' or a 'set' function for working with your struct. @@ -407,38 +484,23 @@ engine.register_type::(); engine.register_get_set("x", TestStruct::get_x, TestStruct::set_x); engine.register_fn("new_ts", TestStruct::new); -if let Ok(result) = engine.eval::("let a = new_ts(); a.x = 500; a.x") { - println!("result: {}", result); -} +let result = engine.eval::("let a = new_ts(); a.x = 500; a.x")?; + +println!("result: {}", result); ``` -### WARNING: Gotcha's with Getters - -When you _get_ a property, the value is cloned. Any update to it downstream will **NOT** be reflected back to the custom type. - -This can introduce subtle bugs. For example: - -```rust -fn change(s) { - s = 42; -} - -let a = new_ts(); -a.x = 500; -a.x.change(); // Only a COPY of 'a.x' is changed. 'a.x' is NOT changed. -a.x == 500; -``` - -# Initializing and maintaining state +Initializing and maintaining state +--------------------------------- By default, Rhai treats each engine invocation as a fresh one, persisting only the functions that have been defined but no top-level state. This gives each one a fairly clean starting place. Sometimes, though, you want to continue using the same top-level state from one invocation to the next. In this example, we first create a state with a few initialized variables, then thread the same state through multiple invocations: ```rust -use rhai::{Engine, Scope}; +use rhai::{Engine, Scope, EvalAltResult}; -fn main() { +fn main() -> Result<(), EvalAltResult> +{ let mut engine = Engine::new(); // First create the state @@ -451,188 +513,212 @@ fn main() { scope.push("z".into(), 999_i64); // First invocation - engine.eval_with_scope::<()>(&mut scope, r" + // (the second boolean argument indicates that we don't need to retain function definitions + // because we didn't declare any!) + engine.eval_with_scope::<()>(&mut scope, false, r" let x = 4 + 5 - y + z; y = 1; - ").expect("y and z not found?"); + ")?; // Second invocation using the same state - if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); // should print 966 - } + let result = engine.eval_with_scope::(&mut scope, false, "x")?; + + println!("result: {}", result); // should print 966 // Variable y is changed in the script - assert_eq!(scope.get_value::("y").unwrap(), 1); + assert_eq!(scope.get_value::("y")?, 1); + + Ok(()) } ``` -# Rhai Language guide +Rhai Language guide +=================== -## Variables +Comments +-------- + +```rust +let /* intruder comment */ name = "Bob"; +// This is a very important comment +/* This comment spans + multiple lines, so it + only makes sense that + it is even more important */ + +/* Fear not, Rhai satisfies all your nesting + needs with nested comments: + /*/*/*/*/**/*/*/*/*/ +*/ +``` + +Variables +--------- + +Variables in Rhai follow normal naming rules (i.e. must contain only ASCII letters, digits and '`_`' underscores). ```rust let x = 3; ``` -## Numeric operators +Numbers +------- + +| Format | Type | +| ---------------- | ---------------------------------------------- | +| `123_345`, `-42` | `i64` in decimal, '`_`' separators are ignored | +| `0o07_76` | `i64` in octal, '`_`' separators are ignored | +| `0xabcd_ef` | `i64` in hex, '`_`' separators are ignored | +| `0b0101_1001` | `i64` in binary, '`_`' separators are ignored | +| `123_456.789` | `f64`, '`_`' separators are ignored | + +Numeric operators +----------------- ```rust -let x = (1 + 2) * (6 - 4) / 2; +let x = (1 + 2) * (6 - 4) / 2; // arithmetic +let reminder = 42 % 10; // modulo +let power = 42 ~ 2; // power (i64 and f64 only) +let left_shifted = 42 << 3; // left shift +let right_shifted = 42 >> 3; // right shift +let bit_op = 42 | 99; // bit masking ``` -## Comparison operators - -You can compare most values of the same data type. If you compare two values of _different_ data types, the result is always `false`. +Unary operators +--------------- ```rust -42 == 42; // true -42 > 42; // false -"hello" > "foo"; // true -"42" == 42; // false -42 == 42.0; // false - i64 is different from f64 +let number = -5; +number = -5 - +5; +let booly = !true; ``` -## Boolean operators +Numeric functions +----------------- -Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. +The following standard functions (defined in the standard library but excluded if `no_stdlib`) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: -Single boolean operators `&` and `|` always evaluate both operands. +| Function | Description | +| ---------- | ----------------------------------- | +| `abs` | absolute value | +| `to_int` | converts an `f32` or `f64` to `i64` | +| `to_float` | converts an integer type to `f64` | + +Floating-point functions +------------------------ + +The following standard functions (defined in the standard library but excluded if `no_stdlib`) operate on `f64` only: + +| Category | Functions | +| ---------------- | ------------------------------------------------------------ | +| Trigonometry | `sin`, `cos`, `tan`, `sinh`, `cosh`, `tanh` in degrees | +| Arc-trigonometry | `asin`, `acos`, `atan`, `asinh`, `acosh`, `atanh` in degrees | +| Square root | `sqrt` | +| Exponential | `exp` (base _e_) | +| Logarithmic | `ln` (base _e_), `log10` (base 10), `log` (any base) | +| Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` | +| Tests | `is_nan`, `is_finite`, `is_infinite` | + +Strings and Chars +----------------- ```rust -this() || that(); // that() is not evaluated if this() is true -this() && that(); // that() is not evaluated if this() is false +let name = "Bob"; +let middle_initial = 'C'; +let last = "Davis"; -this() | that(); // both this() and that() are evaluated -this() & that(); // both this() and that() are evaluated +let full_name = name + " " + middle_initial + ". " + last; +full_name == "Bob C. Davis"; + +// String building with different types +let age = 42; +let record = full_name + ": age " + age; +record == "Bob C. Davis: age 42"; + +// Strings can be indexed to get a character +let c = record[4]; +c == 'C'; + +ts.s = record; + +let c = ts.s[4]; +c == 'C'; + +let c = "foo"[0]; +c == 'f'; + +let c = ("foo" + "bar")[5]; +c == 'r'; + +// Escape sequences in strings +record += " \u2764\n"; // escape sequence of '❤' in Unicode +record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line + +// Unlike Rust, Rhai strings can be modified +record[4] = '\x58'; // 0x58 = 'X' +record == "Bob X. Davis: age 42 ❤\n"; ``` -## If +The following standard functions (defined in the standard library but excluded if `no_stdlib`) operate on strings: + +| Function | Description | +| ---------- | ------------------------------------------------------------------------ | +| `len` | returns the number of characters (not number of bytes) in the string | +| `pad` | pads the string with an character until a specified number of characters | +| `append` | Adds a character or a string to the end of another string | +| `clear` | empties the string | +| `truncate` | cuts off the string at exactly a specified number of characters | +| `contains` | checks if a certain character or sub-string occurs in the string | +| `replace` | replaces a substring with another | +| `trim` | trims the string | + +Examples: ```rust -if true { - print("It's true!"); -} else if true { - print("It's true again!"); -} else { - print("It's false!"); -} +let full_name == " Bob C. Davis "; +full_name.len() == 14; + +full_name.trim(); +full_name.len() == 12; +full_name == "Bob C. Davis"; + +full_name.pad(15, '$'); +full_name.len() == 15; +full_name == "Bob C. Davis$$$"; + +full_name.truncate(6); +full_name.len() == 6; +full_name == "Bob C."; + +full_name.replace("Bob", "John"); +full_name.len() == 7; +full_name = "John C."; + +full_name.contains('C') == true; +full_name.contains("John") == true; + +full_name.clear(); +full_name.len() == 0; ``` -## While - -```rust -let x = 10; - -while x > 0 { - print(x); - if x == 5 { break; } - x = x - 1; -} -``` - -## Loop - -```rust -let x = 10; - -loop { - print(x); - x = x - 1; - if x == 0 { break; } -} -``` - -## Functions - -Rhai supports defining functions in script: - -```rust -fn add(x, y) { - return x + y; -} - -print(add(2, 3)); -``` - -Just like in Rust, you can also use an implicit return. - -```rust -fn add(x, y) { - x + y -} - -print(add(2, 3)); -``` - -Remember that functions defined in script always take `Dynamic` arguments (i.e. the arguments can be of any type). - -Arguments are passed by value, so all functions are _pure_ (i.e. they never modify their arguments). - -Furthermore, functions can only be defined at the top level, never inside a block or another function. - -```rust -// Top level is OK -fn add(x, y) { - x + y -} - -// The following will not compile -fn do_addition(x) { - fn add_y(n) { // functions cannot be defined inside another function - n + y - } - - add_y(x) -} -``` - -## Return - -```rust -return; - -return 123 + 456; -``` - -## Errors and Exceptions - -```rust -if error != "" { - throw error; // 'throw' takes a string to form the exception text -} - -throw; // no exception text -``` - -All of `Engine`'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. - -Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(reason, position))` with the exception text captured by the `reason` parameter. - -```rust -let result = engine.eval::(&mut scope, r#" - let x = 42; - - if x > 0 { - throw x + " is too large!"; - } -"#); - -println!(result); // prints "Runtime error: 42 is too large! (line 5, position 15)" -``` - -## Arrays +Arrays +------ You can create arrays of values, and then access them with numeric indices. -The following functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on arrays: +The following functions (defined in the standard library but excluded if `no_stdlib`) operate on arrays: -* `push` - inserts an element at the end -* `pop` - removes the last element and returns it (() if empty) -* `shift` - removes the first element and returns it (() if empty) -* `len` - returns the number of elements -* `pad` - pads the array with an element until a specified length -* `clear` - empties the array -* `truncate` - cuts off the array at exactly a specified length (discarding all subsequent elements) +| Function | Description | +| ---------- | ------------------------------------------------------------------------------------- | +| `push` | inserts an element at the end | +| `pop` | removes the last element and returns it (`()` if empty) | +| `shift` | removes the first element and returns it (`()` if empty) | +| `len` | returns the number of elements | +| `pad` | pads the array with an element until a specified length | +| `clear` | empties the array | +| `truncate` | cuts off the array at exactly a specified length (discarding all subsequent elements) | + +Examples: ```rust let y = [1, 2, 3]; // 3 elements @@ -692,185 +778,46 @@ engine.register_fn("push", The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`. -## For loops +Comparison operators +-------------------- + +You can compare most values of the same data type. If you compare two values of _different_ data types, the result is always `false`. ```rust -let array = [1, 3, 5, 7, 9, 42]; - -for x in array { - print(x); - if x == 42 { break; } -} - -// The range function allows iterating from first..last-1 -for x in range(0,50) { - print(x); - if x == 42 { break; } -} +42 == 42; // true +42 > 42; // false +"hello" > "foo"; // true +"42" == 42; // false +42 == 42.0; // false - i64 is different from f64 ``` -## Members and methods +Boolean operators +----------------- + +Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. + +Single boolean operators `&` and `|` always evaluate both operands. ```rust -let a = new_ts(); -a.x = 500; -a.update(); +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 ``` -## Numbers - -```rust -let x = 123; // i64 -let x = 123.4; // f64 -let x = 123_456_789; // separators can be put anywhere inside the number - -let x = 0x12abcd; // i64 in hex -let x = 0o777; // i64 in oct -let x = 0b1010_1111; // i64 in binary -``` - -Conversion functions (defined in the standard library but excluded if you use the `no_stdlib` feature): - -* `to_int` - converts an `f32` or `f64` to `i64` -* `to_float` - converts an integer type to `f64` - -## Strings and Chars - -```rust -let name = "Bob"; -let middle_initial = 'C'; -let last = "Davis"; - -let full_name = name + " " + middle_initial + ". " + last; -full_name == "Bob C. Davis"; - -// String building with different types (not available if 'no_stdlib' features is used) -let age = 42; -let record = full_name + ": age " + age; -record == "Bob C. Davis: age 42"; - -// Strings can be indexed to get a character -let c = record[4]; -c == 'C'; - -ts.s = record; - -let c = ts.s[4]; -c == 'C'; - -let c = "foo"[0]; -c == 'f'; - -let c = ("foo" + "bar")[5]; -c == 'r'; - -// Escape sequences in strings -record += " \u2764\n"; // escape sequence of '❤' in Unicode -record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line - -// Unlike Rust, Rhai strings can be modified -record[4] = '\x58'; // 0x58 = 'X' -record == "Bob X. Davis: age 42 ❤\n"; -``` - -The following standard functions (defined in the standard library but excluded if you use the `no_stdlib` feature) operate on strings: - -* `len` - returns the number of characters (not number of bytes) in the string -* `pad` - pads the string with an character until a specified number of characters -* `clear` - empties the string -* `truncate` - cuts off the string at exactly a specified number of characters -* `contains` - checks if a certain character or sub-string occurs in the string -* `replace` - replaces a substring with another -* `trim` - trims the string - -```rust -let full_name == " Bob C. Davis "; -full_name.len() == 14; - -full_name.trim(); -full_name.len() == 12; -full_name == "Bob C. Davis"; - -full_name.pad(15, '$'); -full_name.len() == 15; -full_name == "Bob C. Davis$$$"; - -full_name.truncate(6); -full_name.len() == 6; -full_name == "Bob C."; - -full_name.replace("Bob", "John"); -full_name.len() == 7; -full_name = "John C."; - -full_name.contains('C') == true; -full_name.contains("John") == true; - -full_name.clear(); -full_name.len() == 0; -``` - -## Print and Debug - -```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 -``` - -### Overriding Print and Debug with Callback functions - -```rust -// Any function that takes a &str argument can be used to override print and debug -engine.on_print(|x| println!("hello: {}", x)); -engine.on_debug(|x| println!("DEBUG: {}", x)); - -// Redirect logging output to somewhere else -let mut log: Vec = Vec::new(); -engine.on_print(|x| log.push(format!("log: {}", x))); -engine.on_debug(|x| log.push(format!("DEBUG: {}", x))); - : - eval script - : -println!("{:?}", log); // 'log' captures all the 'print' and 'debug' results. -``` - -## Comments - -```rust -let /* intruder comment */ name = "Bob"; -// This is a very important comment -/* This comment spans - multiple lines, so it - only makes sense that - it is even more important */ - -/* Fear not, Rhai satisfies all your nesting - needs with nested comments: - /*/*/*/*/**/*/*/*/*/ -*/ -``` - -## Unary operators - -```rust -let number = -5; -number = -5 - +5; -let booly = !true; -``` - -## Compound assignment operators +Compound assignment operators +---------------------------- ```rust let number = 5; -number += 4; -number -= 3; -number *= 2; -number /= 1; -number %= 3; -number <<= 2; -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: @@ -882,3 +829,195 @@ my_str += 12345; my_str == "abcABC12345" ``` + +If +-- + +```rust +if true { + print("It's true!"); +} else if true { + print("It's true again!"); +} else { + print("It's false!"); +} +``` + +While +----- + +```rust +let x = 10; + +while x > 0 { + print(x); + if x == 5 { break; } + x = x - 1; +} +``` + +Loop +---- + +```rust +let x = 10; + +loop { + print(x); + x = x - 1; + if x == 0 { break; } +} +``` + +For +--- + +```rust +let array = [1, 3, 5, 7, 9, 42]; + +// Iterate through array +for x in array { + print(x); + if x == 42 { break; } +} + +// The 'range' function allows iterating from first..last-1 +for x in range(0, 50) { + print(x); + if x == 42 { break; } +} +``` + +Return +------ + +```rust +return; // equivalent to return (); + +return 123 + 456; +``` + +Errors and Exceptions +--------------------- + +```rust +if some_bad_condition_has_happened { + throw error; // 'throw' takes a string to form the exception text +} + +throw; // no exception text +``` + +All of `Engine`'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. + +Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(reason, position))` with the exception text captured by the `reason` parameter. + +```rust +let result = engine.eval::(&mut scope, r#" + let x = 42; + + if x > 0 { + throw x + " is too large!"; + } +"#); + +println!(result); // prints "Runtime error: 42 is too large! (line 5, position 15)" +``` + +Functions +--------- + +Rhai supports defining functions in script: + +```rust +fn add(x, y) { + return x + y; +} + +print(add(2, 3)); +``` + +Just like in Rust, you can also use an implicit return. + +```rust +fn add(x, y) { + x + y +} + +print(add(2, 3)); +``` + +Remember that functions defined in script always take `Dynamic` arguments (i.e. the arguments can be of any type). + +However, all arguments are passed by _value_, so all functions are _pure_ (i.e. they never modify their arguments). +Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if you are not careful. + +```rust +fn change(s) { + s = 42; // only a COPY of 'x' is changed +} + +let x = 500; +x.change(); +x == 500; // 'x' is NOT changed! +``` + +Furthermore, functions can only be defined at the top level, never inside a block or another function. + +```rust +// Top level is OK +fn add(x, y) { + x + y +} + +// The following will not compile +fn do_addition(x) { + fn add_y(n) { // functions cannot be defined inside another function + n + y + } + + add_y(x) +} +``` + +Members and methods +------------------- + +```rust +let a = new_ts(); +a.x = 500; +a.update(); +``` + +`print` and `debug` +------------------- + +```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 +``` + +### Overriding `print` and `debug` with callback functions + +```rust +// Any function or closure that takes an &str argument can be used to override print and debug +engine.on_print(|x| println!("hello: {}", x)); +engine.on_debug(|x| println!("DEBUG: {}", x)); + +// Example: quick-'n-dirty logging +let mut log: Vec = Vec::new(); + +// Redirect print/debug output to 'log' +engine.on_print(|s| log.push(format!("entry: {}", s))); +engine.on_debug(|s| log.push(format!("DEBUG: {}", s))); + +// Evalulate script +engine.eval::<()>(script)?; + +// 'log' captures all the 'print' and 'debug' output +for entry in log { + println!("{}", entry); +} +``` diff --git a/examples/custom_types_and_methods.rs b/examples/custom_types_and_methods.rs index 85cb2b60..4471cadf 100644 --- a/examples/custom_types_and_methods.rs +++ b/examples/custom_types_and_methods.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult, RegisterFn}; #[derive(Clone)] struct TestStruct { @@ -15,7 +15,7 @@ impl TestStruct { } } -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); engine.register_type::(); @@ -23,7 +23,9 @@ fn main() { engine.register_fn("update", TestStruct::update); engine.register_fn("new_ts", TestStruct::new); - if let Ok(result) = engine.eval::("let x = new_ts(); x.update(); x") { - println!("result: {}", result.x); // prints 1001 - } + let result = engine.eval::("let x = new_ts(); x.update(); x")?; + + println!("result: {}", result.x); // prints 1001 + + Ok(()) } diff --git a/examples/hello.rs b/examples/hello.rs index cb889fe5..590074e7 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,9 +1,11 @@ -use rhai::Engine; +use rhai::{Engine, EvalAltResult}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - if let Ok(result) = engine.eval::("40 + 2") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("40 + 2")?; + + println!("Answer: {}", result); // prints 42 + + Ok(()) } diff --git a/examples/repl.rs b/examples/repl.rs index 1990afed..a4221614 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -1,25 +1,66 @@ -use rhai::{Engine, RegisterFn, Scope}; -use std::io::{stdin, stdout, Write}; -use std::process::exit; +use rhai::{Engine, EvalAltResult, Scope}; +use std::{ + io::{stdin, stdout, Write}, + iter, +}; -pub fn main() { - let mut engine = Engine::new(); - let mut scope = Scope::new(); +fn print_error(input: &str, err: EvalAltResult) { + fn padding(pad: &str, len: usize) -> String { + iter::repeat(pad).take(len).collect::() + } - engine.register_fn("exit", || exit(0)); + let lines: Vec<_> = input.split("\n").collect(); - loop { - print!("> "); - - let mut input = String::new(); - stdout().flush().expect("couldn't flush stdout"); - - if let Err(e) = stdin().read_line(&mut input) { - println!("input error: {}", e); + // Print error + match err.position() { + p if p.is_eof() => { + // EOF + let last = lines[lines.len() - 2]; + println!("{}", last); + println!("{}^ {}", padding(" ", last.len() - 1), err); } + p if p.is_none() => { + // No position + println!("{}", err); + } + p => { + // Specific position + let pos_text = format!( + " (line {}, position {})", + p.line().unwrap(), + p.position().unwrap() + ); - if let Err(e) = engine.consume_with_scope(&mut scope, &input) { - println!("error: {}", e); + println!("{}", lines[p.line().unwrap() - 1]); + println!( + "{}^ {}", + padding(" ", p.position().unwrap() - 1), + err.to_string().replace(&pos_text, "") + ); + } + } +} + +fn main() { + let mut engine = Engine::new(); + let mut scope = Scope::new(); + + let mut input = String::new(); + + loop { + print!("rhai> "); + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + if let Err(err) = stdin().read_line(&mut input) { + println!("input error: {}", err); + } + + if let Err(err) = engine.consume_with_scope(&mut scope, true, &input) { + println!(""); + print_error(&input, err); + println!(""); } } } diff --git a/examples/reuse_scope.rs b/examples/reuse_scope.rs index 071eb9f3..c83d0a72 100644 --- a/examples/reuse_scope.rs +++ b/examples/reuse_scope.rs @@ -1,14 +1,14 @@ -use rhai::{Engine, Scope}; +use rhai::{Engine, EvalAltResult, Scope}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); let mut scope = Scope::new(); - assert!(engine - .eval_with_scope::<()>(&mut scope, "let x = 4 + 5") - .is_ok()); + engine.eval_with_scope::<()>(&mut scope, false, "let x = 4 + 5")?; - if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); - } + let result = engine.eval_with_scope::(&mut scope, false, "x")?; + + println!("result: {}", result); + + Ok(()) } diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 2e0da794..d535f078 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -1,27 +1,79 @@ -use rhai::{Engine, RegisterFn}; -use std::env; -use std::fmt::Display; +use rhai::{Engine, EvalAltResult}; +use std::{env, fs::File, io::Read, iter, process::exit}; -fn showit(x: &mut T) -> () { - println!("{}", x) +fn padding(pad: &str, len: usize) -> String { + iter::repeat(pad).take(len).collect::() } -fn main() { - for fname in env::args().skip(1) { - let mut engine = Engine::new(); +fn eprint_error(input: &str, err: EvalAltResult) { + fn eprint_line(lines: &Vec<&str>, line: usize, pos: usize, err: &str) { + let line_no = format!("{}: ", line); + let pos_text = format!(" (line {}, position {})", line, pos); - engine.register_fn("print", showit as fn(x: &mut i32) -> ()); - engine.register_fn("print", showit as fn(x: &mut i64) -> ()); - engine.register_fn("print", showit as fn(x: &mut u32) -> ()); - engine.register_fn("print", showit as fn(x: &mut u64) -> ()); - engine.register_fn("print", showit as fn(x: &mut f32) -> ()); - engine.register_fn("print", showit as fn(x: &mut f64) -> ()); - engine.register_fn("print", showit as fn(x: &mut bool) -> ()); - engine.register_fn("print", showit as fn(x: &mut String) -> ()); + eprintln!("{}{}", line_no, lines[line - 1]); + eprintln!( + "{}^ {}", + padding(" ", line_no.len() + pos - 1), + err.replace(&pos_text, "") + ); + eprintln!(""); + } - match engine.eval_file::<()>(&fname) { - Ok(_) => (), - Err(e) => println!("Error: {}", e), + let lines: Vec<_> = input.split("\n").collect(); + + // Print error + match err.position() { + p if p.is_eof() => { + // EOF + let line = lines.len() - 1; + let pos = lines[line - 1].len(); + eprint_line(&lines, line, pos, &err.to_string()); + } + p if p.is_none() => { + // No position + eprintln!("{}", err); + } + p => { + // Specific position + eprint_line( + &lines, + p.line().unwrap(), + p.position().unwrap(), + &err.to_string(), + ) + } + } +} + +fn main() { + for filename in env::args().skip(1) { + let mut engine = Engine::new(); + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!("Error reading script file: {}\n{}", filename, err); + exit(1); + } + Ok(f) => f, + }; + + let mut contents = String::new(); + + match f.read_to_string(&mut contents) { + Err(err) => { + eprintln!("Error reading script file: {}\n{}", filename, err); + exit(1); + } + _ => (), + } + + if let Err(err) = engine.consume(&contents) { + eprintln!("{}", padding("=", filename.len())); + eprintln!("{}", filename); + eprintln!("{}", padding("=", filename.len())); + eprintln!(""); + + eprint_error(&contents, err); } } } diff --git a/examples/simple_fn.rs b/examples/simple_fn.rs index 4be5e5b6..994eb7ec 100644 --- a/examples/simple_fn.rs +++ b/examples/simple_fn.rs @@ -1,6 +1,6 @@ -use rhai::{Engine, RegisterFn}; +use rhai::{Engine, EvalAltResult, RegisterFn}; -fn main() { +fn main() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); fn add(x: i64, y: i64) -> i64 { @@ -9,7 +9,9 @@ fn main() { engine.register_fn("add", add); - if let Ok(result) = engine.eval::("add(40, 2)") { - println!("Answer: {}", result); // prints 42 - } + let result = engine.eval::("add(40, 2)")?; + + println!("Answer: {}", result); // prints 42 + + Ok(()) } diff --git a/src/any.rs b/src/any.rs index c53c8c74..96fe099b 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,5 +1,9 @@ -use std::any::{type_name, TypeId}; -use std::fmt; +//! Helper module which defines the `Any` trait to to allow dynamic value handling. + +use std::{ + any::{type_name, TypeId}, + fmt, +}; /// An raw value of any type. pub type Variant = dyn Any; diff --git a/src/api.rs b/src/api.rs index b27378f2..5e89c5ca 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,3 +1,5 @@ +//! Module that defines the extern API of `Engine`. + use crate::any::{Any, AnyExt, Dynamic}; use crate::call::FuncArgs; use crate::engine::{Engine, FnAny, FnIntExt, FnSpec}; @@ -6,10 +8,14 @@ use crate::fn_register::RegisterFn; use crate::parser::{lex, parse, Position, AST}; use crate::result::EvalAltResult; use crate::scope::Scope; -use std::any::TypeId; -use std::sync::Arc; +use std::{ + any::{type_name, TypeId}, + fs::File, + io::prelude::*, + sync::Arc, +}; -impl<'a> Engine<'a> { +impl<'e> Engine<'e> { pub(crate) fn register_fn_raw( &mut self, fn_name: &str, @@ -17,12 +23,16 @@ impl<'a> Engine<'a> { f: Box, ) { debug_println!( - "Register function: {} for {} parameter(s)", + "Register function: {} with {}", fn_name, if let Some(a) = &args { - format!("{}", a.len()) + format!( + "{} parameter{}", + a.len(), + if a.len() > 1 { "s" } else { "" } + ) } else { - "no".to_string() + "no parameter".to_string() } ); @@ -31,24 +41,21 @@ impl<'a> Engine<'a> { args, }; - self.external_functions - .insert(spec, Arc::new(FnIntExt::Ext(f))); + self.ext_functions.insert(spec, Arc::new(FnIntExt::Ext(f))); } /// Register a custom type for use with the `Engine`. /// The type must be `Clone`. pub fn register_type(&mut self) { - self.register_type_with_name::(std::any::type_name::()); + self.register_type_with_name::(type_name::()); } /// Register a custom type for use with the `Engine` with a name for the `type_of` function. /// The type must be `Clone`. - pub fn register_type_with_name(&mut self, type_name: &str) { + pub fn register_type_with_name(&mut self, name: &str) { // Add the pretty-print type name into the map - self.type_names.insert( - std::any::type_name::().to_string(), - type_name.to_string(), - ); + self.type_names + .insert(type_name::().to_string(), name.to_string()); } /// Register an iterator adapter for a type with the `Engine`. @@ -91,17 +98,14 @@ impl<'a> Engine<'a> { self.register_set(name, set_fn); } - /// Compile a string into an AST - pub fn compile(input: &str) -> Result { + /// Compile a string into an AST. + pub fn compile(&self, input: &str) -> Result { let tokens = lex(input); - parse(&mut tokens.peekable()) + parse(&mut tokens.peekable(), self.optimize) } - /// Compile a file into an AST - pub fn compile_file(filename: &str) -> Result { - use std::fs::File; - use std::io::prelude::*; - + /// Compile a file into an AST. + pub fn compile_file(&self, filename: &str) -> Result { let mut f = File::open(filename) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err))?; @@ -109,14 +113,11 @@ impl<'a> Engine<'a> { f.read_to_string(&mut contents) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err)) - .and_then(|_| Self::compile(&contents).map_err(EvalAltResult::ErrorParsing)) + .and_then(|_| self.compile(&contents).map_err(EvalAltResult::ErrorParsing)) } - /// Evaluate a file + /// Evaluate a file. pub fn eval_file(&mut self, filename: &str) -> Result { - use std::fs::File; - use std::io::prelude::*; - let mut f = File::open(filename) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err))?; @@ -127,32 +128,40 @@ impl<'a> Engine<'a> { .and_then(|_| self.eval::(&contents)) } - /// Evaluate a string + /// Evaluate a string. 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, false, input) } - /// Evaluate a string with own scope + /// Evaluate a string with own scope. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn eval_with_scope( &mut self, scope: &mut Scope, + retain_functions: bool, input: &str, ) -> Result { - let ast = Self::compile(input).map_err(EvalAltResult::ErrorParsing)?; - self.eval_ast_with_scope(scope, &ast) + let ast = self.compile(input).map_err(EvalAltResult::ErrorParsing)?; + self.eval_ast_with_scope(scope, retain_functions, &ast) } - /// Evaluate an AST + /// Evaluate an AST. 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, false, ast) } - /// Evaluate an AST with own scope + /// Evaluate an AST with own scope. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn eval_ast_with_scope( &mut self, scope: &mut Scope, + retain_functions: bool, ast: &AST, ) -> Result { let AST(statements, functions) = ast; @@ -169,9 +178,11 @@ impl<'a> Engine<'a> { let result = statements .iter() - .try_fold(().into_dynamic(), |_, o| self.eval_stmt(scope, o)); + .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt)); - self.script_functions.clear(); // Clean up engine + if !retain_functions { + self.clear_functions(); + } match result { Err(EvalAltResult::Return(out, pos)) => out.downcast::().map(|v| *v).map_err(|a| { @@ -193,12 +204,8 @@ impl<'a> Engine<'a> { } /// 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 + /// Useful for when you don't need the result, but still need to keep track of possible errors. pub fn consume_file(&mut self, filename: &str) -> Result<(), EvalAltResult> { - use std::fs::File; - use std::io::prelude::*; - let mut f = File::open(filename) .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err))?; @@ -210,23 +217,25 @@ impl<'a> Engine<'a> { } /// 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 + /// Useful for when you don't need the result, but still need to keep track of possible errors. pub fn consume(&mut self, input: &str) -> Result<(), EvalAltResult> { - self.consume_with_scope(&mut Scope::new(), input) + self.consume_with_scope(&mut Scope::new(), false, input) } - /// 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 + /// 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. pub fn consume_with_scope( &mut self, scope: &mut Scope, + retain_functions: bool, input: &str, ) -> Result<(), EvalAltResult> { let tokens = lex(input); - parse(&mut tokens.peekable()) + parse(&mut tokens.peekable(), self.optimize) .map_err(|err| EvalAltResult::ErrorParsing(err)) .and_then(|AST(ref statements, ref functions)| { for f in functions { @@ -244,7 +253,9 @@ impl<'a> Engine<'a> { .try_fold(().into_dynamic(), |_, o| self.eval_stmt(scope, o)) .map(|_| ()); - self.script_functions.clear(); // Clean up engine + if !retain_functions { + self.clear_functions(); + } val }) @@ -255,13 +266,14 @@ impl<'a> Engine<'a> { /// # Example /// /// ```rust - /// # use rhai::{Engine, EvalAltResult}; - /// # fn main() -> Result<(), EvalAltResult> { + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// /// let mut engine = Engine::new(); /// - /// let ast = Engine::compile("fn add(x, y) { x.len() + y }")?; + /// let ast = engine.compile("fn add(x, y) { x.len() + y }")?; /// - /// let result: i64 = engine.call_fn("add", ast, (&mut String::from("abc"), &mut 123_i64))?; + /// let result: i64 = engine.call_fn("add", &ast, (&mut String::from("abc"), &mut 123_i64))?; /// /// assert_eq!(result, 126); /// # Ok(()) @@ -270,7 +282,7 @@ impl<'a> Engine<'a> { pub fn call_fn<'f, A: FuncArgs<'f>, T: Any + Clone>( &mut self, name: &str, - ast: AST, + ast: &AST, args: A, ) -> Result { let pos = Default::default(); @@ -296,7 +308,7 @@ impl<'a> Engine<'a> { }) }); - self.script_functions.clear(); // Clean up engine + self.clear_functions(); result } @@ -306,18 +318,22 @@ impl<'a> Engine<'a> { /// # Example /// /// ```rust - /// # use rhai::Engine; + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// /// let mut result = String::from(""); /// { /// let mut engine = Engine::new(); /// /// // Override action of 'print' function /// engine.on_print(|s| result.push_str(s)); - /// engine.consume("print(40 + 2);").unwrap(); + /// engine.consume("print(40 + 2);")?; /// } /// assert_eq!(result, "42"); + /// # Ok(()) + /// # } /// ``` - pub fn on_print(&mut self, callback: impl FnMut(&str) + 'a) { + pub fn on_print(&mut self, callback: impl FnMut(&str) + 'e) { self.on_print = Box::new(callback); } @@ -326,18 +342,22 @@ impl<'a> Engine<'a> { /// # Example /// /// ```rust - /// # use rhai::Engine; + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// /// let mut result = String::from(""); /// { /// let mut engine = Engine::new(); /// /// // Override action of 'debug' function /// engine.on_debug(|s| result.push_str(s)); - /// engine.consume(r#"debug("hello");"#).unwrap(); + /// engine.consume(r#"debug("hello");"#)?; /// } /// assert_eq!(result, "\"hello\""); + /// # Ok(()) + /// # } /// ``` - pub fn on_debug(&mut self, callback: impl FnMut(&str) + 'a) { + pub fn on_debug(&mut self, callback: impl FnMut(&str) + 'e) { self.on_debug = Box::new(callback); } } diff --git a/src/builtin.rs b/src/builtin.rs index ba175ea0..6cce507e 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -1,8 +1,29 @@ +//! Helper module that allows registration of the _core library_ and +//! _standard library_ of utility functions. + use crate::any::Any; use crate::engine::{Array, Engine}; use crate::fn_register::RegisterFn; -use std::fmt::{Debug, Display}; -use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Shl, Shr, Sub}; +use std::{ + fmt::{Debug, Display}, + i32, i64, + ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Sub}, + u32, +}; + +#[cfg(feature = "unchecked")] +use std::ops::{Shl, Shr}; + +#[cfg(not(feature = "unchecked"))] +use crate::{parser::Position, result::EvalAltResult, RegisterResultFn}; + +#[cfg(not(feature = "unchecked"))] +use std::convert::TryFrom; + +#[cfg(not(feature = "unchecked"))] +use num_traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedShl, CheckedShr, CheckedSub, +}; macro_rules! reg_op { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( @@ -12,6 +33,24 @@ macro_rules! reg_op { ) } +#[cfg(not(feature = "unchecked"))] +macro_rules! reg_op_result { + ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( + $( + $self.register_result_fn($x, $op as fn(x: $y, y: $y)->Result<$y,EvalAltResult>); + )* + ) +} + +#[cfg(not(feature = "unchecked"))] +macro_rules! reg_op_result1 { + ($self:expr, $x:expr, $op:expr, $v:ty, $( $y:ty ),*) => ( + $( + $self.register_result_fn($x, $op as fn(x: $y, y: $v)->Result<$y,EvalAltResult>); + )* + ) +} + macro_rules! reg_un { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( $( @@ -20,6 +59,14 @@ macro_rules! reg_un { ) } +#[cfg(not(feature = "unchecked"))] +macro_rules! reg_un_result { + ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( + $( + $self.register_result_fn($x, $op as fn(x: $y)->Result<$y,EvalAltResult>); + )* + ) +} macro_rules! reg_cmp { ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( $( @@ -66,21 +113,102 @@ macro_rules! reg_func3 { impl Engine<'_> { /// Register the core built-in library. pub(crate) fn register_core_lib(&mut self) { - fn add(x: T, y: T) -> ::Output { + #[cfg(not(feature = "unchecked"))] + fn add(x: T, y: T) -> Result { + x.checked_add(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Addition overflow: {} + {}", x, y), + Position::none(), + ) + }) + } + #[cfg(not(feature = "unchecked"))] + fn sub(x: T, y: T) -> Result { + x.checked_sub(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Subtraction underflow: {} - {}", x, y), + Position::none(), + ) + }) + } + #[cfg(not(feature = "unchecked"))] + fn mul(x: T, y: T) -> Result { + x.checked_mul(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Multiplication overflow: {} * {}", x, y), + Position::none(), + ) + }) + } + #[cfg(not(feature = "unchecked"))] + fn div(x: T, y: T) -> Result + where + T: Display + CheckedDiv + PartialEq + TryFrom, + { + if y == >::try_from(0) + .map_err(|_| ()) + .expect("zero should always succeed") + { + return Err(EvalAltResult::ErrorArithmetic( + format!("Division by zero: {} / {}", x, y), + Position::none(), + )); + } + + x.checked_div(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Division overflow: {} / {}", x, y), + Position::none(), + ) + }) + } + #[cfg(not(feature = "unchecked"))] + fn neg(x: T) -> Result { + x.checked_neg().ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Negation overflow: -{}", x), + Position::none(), + ) + }) + } + #[cfg(not(feature = "unchecked"))] + fn abs>(x: T) -> Result { + if x >= 0.into() { + Ok(x) + } else { + x.checked_neg().ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Negation overflow: -{}", x), + Position::none(), + ) + }) + } + } + fn add_u(x: T, y: T) -> ::Output { x + y } - fn sub(x: T, y: T) -> ::Output { + fn sub_u(x: T, y: T) -> ::Output { x - y } - fn mul(x: T, y: T) -> ::Output { + fn mul_u(x: T, y: T) -> ::Output { x * y } - fn div(x: T, y: T) -> ::Output { + fn div_u(x: T, y: T) -> ::Output { x / y } - fn neg(x: T) -> ::Output { + fn neg_u(x: T) -> ::Output { -x } + fn abs_u>(x: T) -> T + where + ::Output: Into, + { + if x < 0.into() { + (-x).into() + } else { + x + } + } fn lt(x: T, y: T) -> bool { x < y } @@ -117,29 +245,117 @@ impl Engine<'_> { fn binary_xor(x: T, y: T) -> ::Output { x ^ y } - fn left_shift>(x: T, y: T) -> >::Output { + #[cfg(not(feature = "unchecked"))] + fn shl(x: T, y: i64) -> Result { + if y < 0 { + return Err(EvalAltResult::ErrorArithmetic( + format!("Left-shift by a negative number: {} << {}", x, y), + Position::none(), + )); + } + + CheckedShl::checked_shl(&x, y as u32).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Left-shift overflow: {} << {}", x, y), + Position::none(), + ) + }) + } + #[cfg(not(feature = "unchecked"))] + fn shr(x: T, y: i64) -> Result { + if y < 0 { + return Err(EvalAltResult::ErrorArithmetic( + format!("Right-shift by a negative number: {} >> {}", x, y), + Position::none(), + )); + } + + CheckedShr::checked_shr(&x, y as u32).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Right-shift overflow: {} % {}", x, y), + Position::none(), + ) + }) + } + #[cfg(feature = "unchecked")] + fn shl_u>(x: T, y: T) -> >::Output { x.shl(y) } - fn right_shift>(x: T, y: T) -> >::Output { + #[cfg(feature = "unchecked")] + fn shr_u>(x: T, y: T) -> >::Output { x.shr(y) } - fn modulo>(x: T, y: T) -> >::Output { + #[cfg(not(feature = "unchecked"))] + fn modulo(x: T, y: T) -> Result { + x.checked_rem(&y).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Modulo division overflow: {} % {}", x, y), + Position::none(), + ) + }) + } + fn modulo_u(x: T, y: T) -> ::Output { x % y } + #[cfg(not(feature = "unchecked"))] + fn pow_i64_i64_u(x: i64, y: i64) -> Result { + if y > (u32::MAX as i64) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), + Position::none(), + )); + } + + x.checked_pow(y as u32).ok_or_else(|| { + EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), + Position::none(), + ) + }) + } + #[cfg(feature = "unchecked")] fn pow_i64_i64(x: i64, y: i64) -> i64 { x.pow(y as u32) } fn pow_f64_f64(x: f64, y: f64) -> f64 { x.powf(y) } + #[cfg(not(feature = "unchecked"))] + fn pow_f64_i64_u(x: f64, y: i64) -> Result { + if y > (i32::MAX as i64) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), + Position::none(), + )); + } + + Ok(x.powi(y as i32)) + } + #[cfg(feature = "unchecked")] fn pow_f64_i64(x: f64, y: i64) -> f64 { x.powi(y as i32) } - reg_op!(self, "+", add, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); - reg_op!(self, "-", sub, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); - reg_op!(self, "*", mul, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); - reg_op!(self, "/", div, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64); + #[cfg(not(feature = "unchecked"))] + { + reg_op_result!(self, "+", add, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "-", sub, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "*", mul, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result!(self, "/", div, i8, u8, i16, u16, i32, i64, u32, u64); + } + + #[cfg(feature = "unchecked")] + { + reg_op!(self, "+", add_u, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "-", sub_u, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "*", mul_u, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, "/", div_u, i8, u8, i16, u16, i32, i64, u32, u64); + } + + reg_op!(self, "+", add_u, f32, f64); + reg_op!(self, "-", sub_u, f32, f64); + reg_op!(self, "*", mul_u, f32, f64); + reg_op!(self, "/", div_u, f32, f64); reg_cmp!(self, "<", lt, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64, String, char); reg_cmp!(self, "<=", lte, i8, u8, i16, u16, i32, i64, u32, u64, f32, f64, String, char); @@ -159,15 +375,53 @@ impl Engine<'_> { reg_op!(self, "&", binary_and, i8, u8, i16, u16, i32, i64, u32, u64); reg_op!(self, "&", and, bool); reg_op!(self, "^", binary_xor, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op!(self, "<<", left_shift, i8, u8, i16, u16, i32, i64, u32, u64); - reg_op!(self, ">>", right_shift, i8, u8, i16, u16); - reg_op!(self, ">>", right_shift, i32, i64, u32, u64); - reg_op!(self, "%", modulo, i8, u8, i16, u16, i32, i64, u32, u64); - self.register_fn("~", pow_i64_i64); - self.register_fn("~", pow_f64_f64); - self.register_fn("~", pow_f64_i64); - reg_un!(self, "-", neg, i8, i16, i32, i64, f32, f64); + #[cfg(not(feature = "unchecked"))] + { + reg_op_result1!(self, "<<", shl, i64, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op_result1!(self, ">>", shr, i64, i8, u8, i16, u16); + reg_op_result1!(self, ">>", shr, i64, i32, i64, u32, u64); + reg_op_result!(self, "%", modulo, i8, u8, i16, u16, i32, i64, u32, u64); + } + + #[cfg(feature = "unchecked")] + { + reg_op!(self, "<<", shl_u, i64, i8, u8, i16, u16, i32, i64, u32, u64); + reg_op!(self, ">>", shr_u, i64, i8, u8, i16, u16); + reg_op!(self, ">>", shr_u, i64, i32, i64, u32, u64); + reg_op!(self, "%", modulo_u, i8, u8, i16, u16, i32, i64, u32, u64); + } + + reg_op!(self, "%", modulo_u, f32, f64); + + self.register_fn("~", pow_f64_f64); + + #[cfg(not(feature = "unchecked"))] + { + self.register_result_fn("~", pow_i64_i64_u); + self.register_result_fn("~", pow_f64_i64_u); + } + + #[cfg(feature = "unchecked")] + { + self.register_fn("~", pow_i64_i64); + self.register_fn("~", pow_f64_i64); + } + + #[cfg(not(feature = "unchecked"))] + { + reg_un_result!(self, "-", neg, i8, i16, i32, i64); + reg_un_result!(self, "abs", abs, i8, i16, i32, i64); + } + + #[cfg(feature = "unchecked")] + { + reg_un!(self, "-", neg_u, i8, i16, i32, i64); + reg_un!(self, "abs", abs_u, i8, i16, i32, i64); + } + + reg_un!(self, "-", neg_u, f32, f64); + reg_un!(self, "abs", abs_u, f32, f64); reg_un!(self, "!", not, bool); self.register_fn("+", |x: String, y: String| x + &y); // String + String @@ -216,6 +470,33 @@ impl Engine<'_> { pub(crate) fn register_stdlib(&mut self) { use crate::fn_register::RegisterDynamicFn; + // Advanced math functions + self.register_fn("sin", |x: f64| x.to_radians().sin()); + self.register_fn("cos", |x: f64| x.to_radians().cos()); + self.register_fn("tan", |x: f64| x.to_radians().tan()); + self.register_fn("sinh", |x: f64| x.to_radians().sinh()); + self.register_fn("cosh", |x: f64| x.to_radians().cosh()); + self.register_fn("tanh", |x: f64| x.to_radians().tanh()); + self.register_fn("asin", |x: f64| x.asin().to_degrees()); + self.register_fn("acos", |x: f64| x.acos().to_degrees()); + self.register_fn("atan", |x: f64| x.atan().to_degrees()); + self.register_fn("asinh", |x: f64| x.asinh().to_degrees()); + self.register_fn("acosh", |x: f64| x.acosh().to_degrees()); + self.register_fn("atanh", |x: f64| x.atanh().to_degrees()); + self.register_fn("sqrt", |x: f64| x.sqrt()); + self.register_fn("exp", |x: f64| x.exp()); + self.register_fn("ln", |x: f64| x.ln()); + self.register_fn("log", |x: f64, base: f64| x.log(base)); + self.register_fn("log10", |x: f64| x.log10()); + self.register_fn("floor", |x: f64| x.floor()); + self.register_fn("ceiling", |x: f64| x.ceil()); + self.register_fn("round", |x: f64| x.ceil()); + self.register_fn("int", |x: f64| x.trunc()); + self.register_fn("fraction", |x: f64| x.fract()); + self.register_fn("is_nan", |x: f64| x.is_nan()); + self.register_fn("is_finite", |x: f64| x.is_finite()); + self.register_fn("is_infinite", |x: f64| x.is_infinite()); + // Register conversion functions self.register_fn("to_float", |x: i8| x as f64); self.register_fn("to_float", |x: u8| x as f64); @@ -234,11 +515,38 @@ impl Engine<'_> { self.register_fn("to_int", |x: i32| x as i64); self.register_fn("to_int", |x: u32| x as i64); self.register_fn("to_int", |x: u64| x as i64); - self.register_fn("to_int", |x: f32| x as i64); - self.register_fn("to_int", |x: f64| x as i64); - self.register_fn("to_int", |ch: char| ch as i64); + #[cfg(not(feature = "unchecked"))] + { + self.register_result_fn("to_int", |x: f32| { + if x > (i64::MAX as f32) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Integer overflow: to_int({})", x), + Position::none(), + )); + } + + Ok(x.trunc() as i64) + }); + self.register_result_fn("to_int", |x: f64| { + if x > (i64::MAX as f64) { + return Err(EvalAltResult::ErrorArithmetic( + format!("Integer overflow: to_int({})", x), + Position::none(), + )); + } + + Ok(x.trunc() as i64) + }); + } + + #[cfg(feature = "unchecked")] + { + self.register_fn("to_int", |x: f32| x as i64); + self.register_fn("to_int", |x: f64| x as i64); + } + // Register array utility functions fn push(list: &mut Array, item: T) { list.push(Box::new(item)); @@ -303,6 +611,8 @@ impl Engine<'_> { self.register_fn("contains", |s: &mut String, ch: char| s.contains(ch)); self.register_fn("contains", |s: &mut String, find: String| s.contains(&find)); self.register_fn("clear", |s: &mut String| s.clear()); + self.register_fn("append", |s: &mut String, ch: char| s.push(ch)); + self.register_fn("append", |s: &mut String, add: String| s.push_str(&add)); self.register_fn("truncate", |s: &mut String, len: i64| { if len >= 0 { let chars: Vec<_> = s.chars().take(len as usize).collect(); diff --git a/src/call.rs b/src/call.rs index b287f2bf..065cc8d5 100644 --- a/src/call.rs +++ b/src/call.rs @@ -1,5 +1,4 @@ -//! Helper module which defines `FnArgs` -//! to make function calling easier. +//! Helper module which defines `FnArgs` to make function calling easier. use crate::any::{Any, Variant}; diff --git a/src/engine.rs b/src/engine.rs index b00db1f9..89dac01c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,25 +1,35 @@ -use std::any::TypeId; -use std::borrow::Cow; -use std::cmp::{PartialEq, PartialOrd}; -use std::collections::HashMap; -use std::sync::Arc; +//! Main module defining the script evaluation `Engine`. use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::parser::{Expr, FnDef, Position, Stmt}; use crate::result::EvalAltResult; use crate::scope::Scope; +use std::{ + any::{type_name, TypeId}, + borrow::Cow, + cmp::{PartialEq, PartialOrd}, + collections::HashMap, + iter::once, + sync::Arc, +}; /// An dynamic array of `Dynamic` values. pub type Array = Vec; pub type FnCallArgs<'a> = Vec<&'a mut Variant>; -const KEYWORD_PRINT: &'static str = "print"; -const KEYWORD_DEBUG: &'static str = "debug"; -const KEYWORD_TYPE_OF: &'static str = "type_of"; +pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; + +type IteratorFn = dyn Fn(&Dynamic) -> Box>; + +pub(crate) const KEYWORD_PRINT: &'static str = "print"; +pub(crate) const KEYWORD_DEBUG: &'static str = "debug"; +pub(crate) const KEYWORD_TYPE_OF: &'static str = "type_of"; +pub(crate) const FUNC_GETTER: &'static str = "get$"; +pub(crate) const FUNC_SETTER: &'static str = "set$"; #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -enum VariableType { +enum IndexSourceType { Array, String, Expression, @@ -31,49 +41,85 @@ pub struct FnSpec<'a> { pub args: Option>, } -type IteratorFn = dyn Fn(&Dynamic) -> Box>; - /// Rhai main scripting engine. /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// -/// fn main() { -/// let mut engine = Engine::new(); +/// let mut engine = Engine::new(); /// -/// if let Ok(result) = engine.eval::("40 + 2") { -/// println!("Answer: {}", result); // prints 42 -/// } -/// } +/// let result = engine.eval::("40 + 2")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` -pub struct Engine<'a> { +pub struct Engine<'e> { + /// Optimize the AST after compilation + pub(crate) optimize: bool, /// A hashmap containing all compiled functions known to the engine - pub(crate) external_functions: HashMap, Arc>, + pub(crate) ext_functions: HashMap, Arc>>, /// A hashmap containing all script-defined functions - pub(crate) script_functions: HashMap, Arc>, + pub(crate) script_functions: HashMap, Arc>>, /// A hashmap containing all iterators known to the engine pub(crate) type_iterators: HashMap>, pub(crate) type_names: HashMap, - pub(crate) on_print: Box, - pub(crate) on_debug: Box, + // Closures for implementing the print/debug commands + pub(crate) on_print: Box, + pub(crate) on_debug: Box, } -pub enum FnIntExt { +pub enum FnIntExt<'a> { Ext(Box), - Int(FnDef), + Int(FnDef<'a>), } -pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; - impl Engine<'_> { + /// Create a new `Engine` + pub fn new() -> Self { + // User-friendly names for built-in types + let type_names = [ + (type_name::(), "string"), + (type_name::(), "array"), + (type_name::(), "dynamic"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + // Create the new scripting Engine + let mut engine = Engine { + optimize: true, + ext_functions: HashMap::new(), + script_functions: HashMap::new(), + type_iterators: HashMap::new(), + type_names, + on_print: Box::new(default_print), // default print/debug implementations + on_debug: Box::new(default_print), + }; + + engine.register_core_lib(); + + #[cfg(not(feature = "no_stdlib"))] + engine.register_stdlib(); // Register the standard library when no_stdlib is not set + + engine + } + + /// Control whether the `Engine` will optimize an AST after compilation + pub fn set_optimization(&mut self, optimize: bool) { + self.optimize = optimize + } + /// Universal method for calling functions, that are either /// registered with the `Engine` or written in Rhai pub(crate) fn call_fn_raw( &mut self, fn_name: &str, args: FnCallArgs, - def_value: Option<&Dynamic>, + def_val: Option<&Dynamic>, pos: Position, ) -> Result { debug_println!( @@ -92,39 +138,44 @@ impl Engine<'_> { }; // First search in script-defined functions (can override built-in), - // then in built-in's + // then built-in's and external functions let fn_def = self .script_functions .get(&spec) .or_else(|| { spec.args = Some(args.iter().map(|a| Any::type_id(&**a)).collect()); - self.external_functions.get(&spec) + self.ext_functions.get(&spec) }) .map(|f| f.clone()); if let Some(f) = fn_def { match *f { - FnIntExt::Ext(ref f) => { - let r = f(args, pos)?; + // Run external function + FnIntExt::Ext(ref func) => { + let result = func(args, pos)?; + // See if the function match print/debug (which requires special processing) let callback = match spec.name.as_ref() { KEYWORD_PRINT => self.on_print.as_mut(), KEYWORD_DEBUG => self.on_debug.as_mut(), - _ => return Ok(r), + _ => return Ok(result), }; - Ok(callback( - &r.downcast::() - .map(|s| *s) - .unwrap_or("error: not a string".into()), - ) - .into_dynamic()) + let val = &result + .downcast::() + .map(|s| *s) + .unwrap_or("error: not a string".into()); + + Ok(callback(val).into_dynamic()) } - FnIntExt::Int(ref f) => { - if f.params.len() != args.len() { + + // Run script-defined function + FnIntExt::Int(ref func) => { + // First check number of parameters + if func.params.len() != args.len() { return Err(EvalAltResult::ErrorFunctionArgsMismatch( spec.name.into(), - f.params.len(), + func.params.len(), args.len(), pos, )); @@ -133,27 +184,50 @@ impl Engine<'_> { let mut scope = Scope::new(); scope.extend( - f.params + // Put arguments into scope as variables + func.params .iter() .cloned() .zip(args.iter().map(|x| (*x).into_dynamic())), ); - match self.eval_stmt(&mut scope, &*f.body) { + // Evaluate + match self.eval_stmt(&mut scope, &func.body) { + // Convert return statement to return value Err(EvalAltResult::Return(x, _)) => Ok(x), other => other, } } } } else if spec.name == KEYWORD_TYPE_OF && args.len() == 1 { + // Handle `type_of` function Ok(self .map_type_name(args[0].type_name()) .to_string() .into_dynamic()) - } else if let Some(val) = def_value { + } else if spec.name.starts_with(FUNC_GETTER) { + // Getter function not found + Err(EvalAltResult::ErrorDotExpr( + format!( + "- property '{}' unknown or write-only", + &spec.name[FUNC_GETTER.len()..] + ), + pos, + )) + } else if spec.name.starts_with(FUNC_SETTER) { + // Setter function not found + Err(EvalAltResult::ErrorDotExpr( + format!( + "- property '{}' unknown or read-only", + &spec.name[FUNC_SETTER.len()..] + ), + pos, + )) + } else if let Some(val) = def_val { // Return default value Ok(val.clone()) } else { + // Raise error let types_list = args .iter() .map(|x| (*x).type_name()) @@ -174,11 +248,9 @@ impl Engine<'_> { this_ptr: &mut Variant, dot_rhs: &Expr, ) -> Result { - use std::iter::once; - match dot_rhs { // xxx.fn_name(args) - Expr::FunctionCall(fn_name, args, def_value, pos) => { + Expr::FunctionCall(fn_name, args, def_val, pos) => { let mut args: Array = args .iter() .map(|arg| self.eval_expr(scope, arg)) @@ -188,68 +260,143 @@ impl Engine<'_> { .chain(args.iter_mut().map(|b| b.as_mut())) .collect(); - self.call_fn_raw(fn_name, args, def_value.as_ref(), *pos) + self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos) } // xxx.id Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) } - // xxx.lhs[idx_expr] - Expr::Index(lhs, idx_expr) => { - let idx = self.eval_index_value(scope, idx_expr)?; - - let (lhs_value, _) = match lhs.as_ref() { + // xxx.idx_lhs[idx_expr] + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (expr, _) = match idx_lhs.as_ref() { + // xxx.id[idx_expr] Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); ( self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?, *pos, ) } - expr => return Err(EvalAltResult::ErrorDotExpr(expr.position())), + // xxx.???[???][idx_expr] + Expr::Index(_, _, _) => { + (self.get_dot_val_helper(scope, this_ptr, idx_lhs)?, *idx_pos) + } + // Syntax error + _ => { + return Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_rhs.position(), + )) + } }; - Self::get_indexed_value(lhs_value, idx, idx_expr.position()).map(|(v, _)| v) + let idx = self.eval_index_value(scope, idx_expr)?; + self.get_indexed_value(expr, idx, idx_expr.position(), *idx_pos) + .map(|(v, _)| v) } - // xxx.lhs.rhs - Expr::Dot(lhs, rhs) => match lhs.as_ref() { + // xxx.dot_lhs.rhs + Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() { // xxx.id.rhs Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) .and_then(|mut v| self.get_dot_val_helper(scope, v.as_mut(), rhs)) } - // xxx.lhs[idx_expr].rhs - Expr::Index(lhs, idx_expr) => { - let idx = self.eval_index_value(scope, idx_expr)?; - - let (lhs_value, _) = match lhs.as_ref() { + // xxx.idx_lhs[idx_expr].rhs + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (expr, _) = match idx_lhs.as_ref() { + // xxx.id[idx_expr].rhs Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); ( self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?, *pos, ) } - expr => return Err(EvalAltResult::ErrorDotExpr(expr.position())), + // xxx.???[???][idx_expr].rhs + Expr::Index(_, _, _) => { + (self.get_dot_val_helper(scope, this_ptr, idx_lhs)?, *idx_pos) + } + // Syntax error + _ => { + return Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_rhs.position(), + )) + } }; - Self::get_indexed_value(lhs_value, idx, idx_expr.position()).and_then( - |(mut value, _)| self.get_dot_val_helper(scope, value.as_mut(), rhs), - ) + let idx = self.eval_index_value(scope, idx_expr)?; + self.get_indexed_value(expr, idx, idx_expr.position(), *idx_pos) + .and_then(|(mut v, _)| self.get_dot_val_helper(scope, v.as_mut(), rhs)) } // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(lhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_lhs.position(), + )), }, // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(dot_rhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "".to_string(), + dot_rhs.position(), + )), + } + } + + /// Evaluate a dot chain getter + fn get_dot_val( + &mut self, + scope: &mut Scope, + dot_lhs: &Expr, + dot_rhs: &Expr, + ) -> Result { + match dot_lhs { + // id.??? + Expr::Identifier(id, pos) => { + let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; + let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); + + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. + *scope.get_mut(id, src_idx) = target; + + val + } + + // idx_lhs[idx_expr].??? + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (src_type, src, idx, mut target) = + self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; + let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); + + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. + if let Some((id, src_idx)) = src { + Self::update_indexed_var_in_scope( + src_type, + scope, + id, + src_idx, + idx, + target, + idx_lhs.position(), + )?; + } + + val + } + + // {expr}.??? + expr => { + let mut target = self.eval_expr(scope, expr)?; + self.get_dot_val_helper(scope, target.as_mut(), dot_rhs) + } } } @@ -280,48 +427,59 @@ impl Engine<'_> { /// Get the value at the indexed position of a base type fn get_indexed_value( + &self, val: Dynamic, idx: i64, - pos: Position, - ) -> Result<(Dynamic, VariableType), EvalAltResult> { + val_pos: Position, + idx_pos: Position, + ) -> Result<(Dynamic, IndexSourceType), EvalAltResult> { if val.is::() { + // val_array[idx] let arr = val.downcast::().expect("array expected"); if idx >= 0 { arr.get(idx as usize) .cloned() - .map(|v| (v, VariableType::Array)) - .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, pos)) + .map(|v| (v, IndexSourceType::Array)) + .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, val_pos)) } else { - Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, pos)) + Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, val_pos)) } } else if val.is::() { + // val_string[idx] let s = val.downcast::().expect("string expected"); if idx >= 0 { s.chars() .nth(idx as usize) - .map(|ch| (ch.into_dynamic(), VariableType::String)) - .ok_or_else(|| EvalAltResult::ErrorStringBounds(s.chars().count(), idx, pos)) + .map(|ch| (ch.into_dynamic(), IndexSourceType::String)) + .ok_or_else(|| { + EvalAltResult::ErrorStringBounds(s.chars().count(), idx, val_pos) + }) } else { Err(EvalAltResult::ErrorStringBounds( s.chars().count(), idx, - pos, + val_pos, )) } } else { - Err(EvalAltResult::ErrorIndexingType(pos)) + // Error - cannot be indexed + Err(EvalAltResult::ErrorIndexingType( + self.map_type_name(val.type_name()).to_string(), + idx_pos, + )) } } /// Evaluate an index expression - fn eval_index_expr( + fn eval_index_expr<'a>( &mut self, scope: &mut Scope, - lhs: &Expr, + lhs: &'a Expr, idx_expr: &Expr, - ) -> Result<(VariableType, Option<(String, usize)>, usize, Dynamic), EvalAltResult> { + idx_pos: Position, + ) -> Result<(IndexSourceType, Option<(&'a str, usize)>, usize, Dynamic), EvalAltResult> { let idx = self.eval_index_value(scope, idx_expr)?; match lhs { @@ -329,158 +487,212 @@ impl Engine<'_> { Expr::Identifier(id, _) => Self::search_scope( scope, &id, - |val| Self::get_indexed_value(val, idx, idx_expr.position()), + |val| self.get_indexed_value(val, idx, idx_expr.position(), idx_pos), lhs.position(), ) - .map(|(src_idx, (val, source_type))| { - (source_type, Some((id.clone(), src_idx)), idx as usize, val) + .map(|(src_idx, (val, src_type))| { + (src_type, Some((id.as_str(), src_idx)), idx as usize, val) }), // (expr)[idx_expr] - expr => Self::get_indexed_value(self.eval_expr(scope, expr)?, idx, idx_expr.position()) - .map(|(val, _)| (VariableType::Expression, None, idx as usize, val)), + expr => { + let val = self.eval_expr(scope, expr)?; + + self.get_indexed_value(val, idx, idx_expr.position(), idx_pos) + .map(|(v, _)| (IndexSourceType::Expression, None, idx as usize, v)) + } } } /// Replace a character at an index position in a mutable string fn str_replace_char(s: &mut String, idx: usize, new_ch: char) { - // The new character - let ch = s.chars().nth(idx).expect("string index out of bounds"); + let mut chars: Vec = s.chars().collect(); + let ch = *chars.get(idx).expect("string index out of bounds"); // See if changed - if so, update the String - if ch == new_ch { - return; + if ch != new_ch { + chars[idx] = new_ch; + s.clear(); + chars.iter().for_each(|&ch| s.push(ch)); } - - // Collect all the characters after the index - let mut chars: Vec = s.chars().collect(); - chars[idx] = new_ch; - s.clear(); - chars.iter().for_each(|&ch| s.push(ch)); } /// Update the value at an index position in a variable inside the scope - fn update_indexed_variable_in_scope( - source_type: VariableType, + fn update_indexed_var_in_scope( + src_type: IndexSourceType, scope: &mut Scope, id: &str, src_idx: usize, idx: usize, - val: Dynamic, - ) -> Option { - match source_type { - VariableType::Array => { + new_val: Dynamic, + val_pos: Position, + ) -> Result { + match src_type { + // array_id[idx] = val + IndexSourceType::Array => { let arr = scope.get_mut_by_type::(id, src_idx); - Some((arr[idx as usize] = val).into_dynamic()) + Ok((arr[idx as usize] = new_val).into_dynamic()) } - VariableType::String => { + // string_id[idx] = val + IndexSourceType::String => { let s = scope.get_mut_by_type::(id, src_idx); // Value must be a character - let ch = *val + let ch = *new_val .downcast::() - .expect("char value expected to update an index position in a string"); - Some(Self::str_replace_char(s, idx as usize, ch).into_dynamic()) + .map_err(|_| EvalAltResult::ErrorCharMismatch(val_pos))?; + Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic()) } - _ => None, + // All other variable types should be an error + _ => panic!("array or string source type expected for indexing"), } } - /// Evaluate a dot chain getter - fn get_dot_val( - &mut self, - scope: &mut Scope, - dot_lhs: &Expr, - dot_rhs: &Expr, + /// Update the value at an index position + fn update_indexed_value( + mut target: Dynamic, + idx: usize, + new_val: Dynamic, + pos: Position, ) -> Result { - match dot_lhs { - // xxx.??? - Expr::Identifier(id, pos) => { - let (sc_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; - let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. - *scope.get_mut(id, sc_idx) = target; - - value - } - - // lhs[idx_expr].??? - Expr::Index(lhs, idx_expr) => { - let (source_type, src, idx, mut target) = - self.eval_index_expr(scope, lhs, idx_expr)?; - let value = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); - - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. - if let Some((id, src_idx)) = src { - Self::update_indexed_variable_in_scope( - source_type, - scope, - &id, - src_idx, - idx, - target, - ) - .expect("array or string source type expected for indexing"); - } - - value - } - - // {expr}.??? - expr => { - let mut target = self.eval_expr(scope, expr)?; - self.get_dot_val_helper(scope, target.as_mut(), dot_rhs) - } - // Syntax error - //_ => Err(EvalAltResult::ErrorDotExpr(dot_lhs.position())), + 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"); + // 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") } + + Ok(target) } /// Chain-evaluate a dot setter fn set_dot_val_helper( &mut self, + scope: &mut Scope, this_ptr: &mut Variant, dot_rhs: &Expr, - mut source_val: Dynamic, + mut new_val: Dynamic, + val_pos: Position, ) -> Result { match dot_rhs { // xxx.id Expr::Identifier(id, pos) => { - let set_fn_name = format!("set${}", id); + let set_fn_name = format!("{}{}", FUNC_SETTER, id); - self.call_fn_raw( - &set_fn_name, - vec![this_ptr, source_val.as_mut()], - None, - *pos, - ) + self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.as_mut()], None, *pos) } - // xxx.lhs.rhs - Expr::Dot(lhs, rhs) => match lhs.as_ref() { + // xxx.lhs[idx_expr] + // TODO - Allow chaining of indexing! + Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() { + // xxx.id[idx_expr] Expr::Identifier(id, pos) => { - let get_fn_name = format!("get${}", id); + let get_fn_name = format!("{}{}", FUNC_GETTER, id); + + self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) + .and_then(|v| { + let idx = self.eval_index_value(scope, idx_expr)?; + Self::update_indexed_value(v, idx as usize, new_val, val_pos) + }) + .and_then(|mut v| { + let set_fn_name = format!("{}{}", FUNC_SETTER, id); + self.call_fn_raw(&set_fn_name, vec![this_ptr, v.as_mut()], None, *pos) + }) + } + + // All others - syntax error for setters chain + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + *idx_pos, + )), + }, + + // xxx.lhs.{...} + Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { + // xxx.id.rhs + Expr::Identifier(id, pos) => { + let get_fn_name = format!("{}{}", FUNC_GETTER, id); self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) .and_then(|mut v| { - self.set_dot_val_helper(v.as_mut(), rhs, source_val) + self.set_dot_val_helper(scope, v.as_mut(), rhs, new_val, val_pos) .map(|_| v) // Discard Ok return value }) .and_then(|mut v| { - let set_fn_name = format!("set${}", id); + let set_fn_name = format!("{}{}", FUNC_SETTER, id); self.call_fn_raw(&set_fn_name, vec![this_ptr, v.as_mut()], None, *pos) }) } - _ => Err(EvalAltResult::ErrorDotExpr(lhs.position())), + + // xxx.lhs[idx_expr].rhs + // TODO - Allow chaining of indexing! + Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() { + // xxx.id[idx_expr].rhs + Expr::Identifier(id, pos) => { + let get_fn_name = format!("{}{}", FUNC_GETTER, id); + + self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) + .and_then(|v| { + let idx = self.eval_index_value(scope, idx_expr)?; + let (mut target, _) = self.get_indexed_value( + v.clone(), // TODO - Avoid cloning this + idx, + idx_expr.position(), + *idx_pos, + )?; + + self.set_dot_val_helper( + scope, + target.as_mut(), + rhs, + new_val, + val_pos, + )?; + + // 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) + }) + .and_then(|mut v| { + let set_fn_name = format!("{}{}", FUNC_SETTER, id); + + self.call_fn_raw( + &set_fn_name, + vec![this_ptr, v.as_mut()], + None, + *pos, + ) + }) + } + + // All others - syntax error for setters chain + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + *idx_pos, + )), + }, + + // All others - syntax error for setters chain + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + lhs.position(), + )), }, // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(dot_rhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + dot_rhs.position(), + )), } } @@ -490,66 +702,75 @@ impl Engine<'_> { scope: &mut Scope, dot_lhs: &Expr, dot_rhs: &Expr, - source_val: Dynamic, + new_val: Dynamic, + val_pos: Position, ) -> Result { match dot_lhs { // id.??? Expr::Identifier(id, pos) => { - let (sc_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; - let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); + let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; + let val = + self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. - *scope.get_mut(id, sc_idx) = target; + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. + *scope.get_mut(id, src_idx) = target; - value + val } // lhs[idx_expr].??? - Expr::Index(lhs, idx_expr) => { - let (source_type, src, idx, mut target) = - self.eval_index_expr(scope, lhs, idx_expr)?; - let value = self.set_dot_val_helper(target.as_mut(), dot_rhs, source_val); - - // In case the expression mutated `target`, we need to reassign it because - // of the above `clone`. + // TODO - Allow chaining of indexing! + Expr::Index(lhs, idx_expr, idx_pos) => { + let (src_type, src, idx, mut target) = + self.eval_index_expr(scope, lhs, idx_expr, *idx_pos)?; + let val = + self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); + // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some((id, src_idx)) = src { - Self::update_indexed_variable_in_scope( - source_type, + Self::update_indexed_var_in_scope( + src_type, scope, - &id, + id, src_idx, idx, target, - ) - .expect("array or string source_type expected for indexing"); + lhs.position(), + )?; } - value + val } // Syntax error - _ => Err(EvalAltResult::ErrorDotExpr(dot_lhs.position())), + _ => Err(EvalAltResult::ErrorDotExpr( + "for assignment".to_string(), + dot_lhs.position(), + )), } } /// Evaluate an expression fn eval_expr(&mut self, scope: &mut Scope, expr: &Expr) -> Result { match expr { - Expr::IntegerConstant(i, _) => Ok((*i).into_dynamic()), - Expr::FloatConstant(i, _) => Ok((*i).into_dynamic()), + Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()), + Expr::FloatConstant(f, _) => Ok(f.into_dynamic()), Expr::StringConstant(s, _) => Ok(s.into_dynamic()), - Expr::CharConstant(c, _) => Ok((*c).into_dynamic()), + Expr::CharConstant(c, _) => Ok(c.into_dynamic()), Expr::Identifier(id, pos) => { Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val) } - Expr::Index(lhs, idx_expr) => self - .eval_index_expr(scope, lhs, idx_expr) + + // lhs[idx_expr] + Expr::Index(lhs, idx_expr, idx_pos) => self + .eval_index_expr(scope, lhs, idx_expr, *idx_pos) .map(|(_, _, _, x)| x), + // Statement block + Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt), + // lhs = rhs - Expr::Assignment(lhs, rhs) => { + Expr::Assignment(lhs, rhs, _) => { let rhs_val = self.eval_expr(scope, rhs)?; match lhs.as_ref() { @@ -564,30 +785,30 @@ impl Engine<'_> { } // idx_lhs[idx_expr] = rhs - Expr::Index(idx_lhs, idx_expr) => { - let (source_type, src, idx, _) = - self.eval_index_expr(scope, idx_lhs, idx_expr)?; + Expr::Index(idx_lhs, idx_expr, idx_pos) => { + let (src_type, src, idx, _) = + self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; if let Some((id, src_idx)) = src { - Self::update_indexed_variable_in_scope( - source_type, + Ok(Self::update_indexed_var_in_scope( + src_type, scope, &id, src_idx, idx, rhs_val, - ) + rhs.position(), + )?) } else { - None + Err(EvalAltResult::ErrorAssignmentToUnknownLHS( + idx_lhs.position(), + )) } - .ok_or_else(|| { - EvalAltResult::ErrorAssignmentToUnknownLHS(idx_lhs.position()) - }) } // dot_lhs.dot_rhs = rhs - Expr::Dot(dot_lhs, dot_rhs) => { - self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val) + Expr::Dot(dot_lhs, dot_rhs, _) => { + self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position()) } // Syntax error @@ -595,7 +816,7 @@ impl Engine<'_> { } } - Expr::Dot(lhs, rhs) => self.get_dot_val(scope, lhs, rhs), + Expr::Dot(lhs, rhs, _) => self.get_dot_val(scope, lhs, rhs), Expr::Array(contents, _) => { let mut arr = Vec::new(); @@ -611,7 +832,7 @@ impl Engine<'_> { Ok(Box::new(arr)) } - Expr::FunctionCall(fn_name, args, def_value, pos) => { + Expr::FunctionCall(fn_name, args, def_val, pos) => { let mut args = args .iter() .map(|expr| self.eval_expr(scope, expr)) @@ -620,7 +841,7 @@ impl Engine<'_> { self.call_fn_raw( fn_name, args.iter_mut().map(|b| b.as_mut()).collect(), - def_value.as_ref(), + def_val.as_ref(), *pos, ) } @@ -670,9 +891,14 @@ impl Engine<'_> { stmt: &Stmt, ) -> Result { match stmt { + // No-op + Stmt::Noop(_) => Ok(().into_dynamic()), + + // Expression as statement Stmt::Expr(expr) => self.eval_expr(scope, expr), - Stmt::Block(block) => { + // Block scope + Stmt::Block(block, _) => { let prev_len = scope.len(); let mut last_result: Result = Ok(().into_dynamic()); @@ -685,13 +911,12 @@ impl Engine<'_> { } } - while scope.len() > prev_len { - scope.pop(); - } + scope.rewind(prev_len); last_result } + // If-else statement Stmt::IfElse(guard, body, else_body) => self .eval_expr(scope, guard)? .downcast::() @@ -706,6 +931,7 @@ impl Engine<'_> { } }), + // While loop Stmt::While(guard, body) => loop { match self.eval_expr(scope, guard)?.downcast::() { Ok(guard_val) => { @@ -723,6 +949,7 @@ impl Engine<'_> { } }, + // Loop statement Stmt::Loop(body) => loop { match self.eval_stmt(scope, body) { Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), @@ -731,6 +958,7 @@ impl Engine<'_> { } }, + // For loop Stmt::For(name, expr, body) => { let arr = self.eval_expr(scope, expr)?; let tid = Any::type_id(&*arr); @@ -755,6 +983,7 @@ impl Engine<'_> { } } + // Break statement Stmt::Break(_) => Err(EvalAltResult::LoopBreak), // Empty return @@ -783,6 +1012,7 @@ impl Engine<'_> { )) } + // Let statement Stmt::Let(name, init, _) => { if let Some(v) = init { let val = self.eval_expr(scope, v)?; @@ -803,36 +1033,9 @@ impl Engine<'_> { .unwrap_or(name) } - /// Make a new engine - pub fn new<'a>() -> Engine<'a> { - use std::any::type_name; - - // User-friendly names for built-in types - let type_names = [ - (type_name::(), "string"), - (type_name::(), "array"), - (type_name::(), "dynamic"), - ] - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - - // Create the new scripting Engine - let mut engine = Engine { - external_functions: HashMap::new(), - script_functions: HashMap::new(), - type_iterators: HashMap::new(), - type_names, - on_print: Box::new(default_print), // default print/debug implementations - on_debug: Box::new(default_print), - }; - - engine.register_core_lib(); - - #[cfg(not(feature = "no_stdlib"))] - engine.register_stdlib(); // Register the standard library when no_stdlib is not set - - engine + /// Clean up all script-defined functions within the `Engine`. + pub fn clear_functions(&mut self) { + self.script_functions.clear(); } } diff --git a/src/error.rs b/src/error.rs index 3096040e..8733682b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ +//! Module containing error definitions for the parsing process. + use crate::parser::Position; -use std::char; -use std::error::Error; -use std::fmt; +use std::{char, error::Error, fmt}; /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Hash, Clone)] @@ -64,9 +64,9 @@ pub enum ParseErrorType { /// An open `[` is missing the corresponding closing `]`. MissingRightBracket(String), /// An expression in function call arguments `()` has syntax error. - MalformedCallExpr, + MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. - MalformedIndexExpr, + MalformedIndexExpr(String), /// Missing a variable name after the `let` keyword. VarExpectsIdentifier, /// Defining a function `fn` in an appropriate place (e.g. inside another function). @@ -75,11 +75,13 @@ pub enum ParseErrorType { FnMissingName, /// A function definition is missing the parameters list. Wrapped value is the function name. FnMissingParams(String), + /// Assignment to an inappropriate LHS (left-hand-side) expression. + AssignmentToInvalidLHS, } /// Error when parsing a script. #[derive(Debug, PartialEq, Clone)] -pub struct ParseError(ParseErrorType, Position); +pub struct ParseError(pub(crate) ParseErrorType, pub(crate) Position); impl ParseError { /// Create a new `ParseError`. @@ -108,12 +110,13 @@ impl Error for ParseError { ParseErrorType::MissingLeftBrace => "Expecting '{'", ParseErrorType::MissingRightBrace(_) => "Expecting '}'", ParseErrorType::MissingRightBracket(_) => "Expecting ']'", - ParseErrorType::MalformedCallExpr => "Invalid expression in function call arguments", - ParseErrorType::MalformedIndexExpr => "Invalid index in indexing expression", + ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", + ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable", ParseErrorType::FnMissingName => "Expecting name in function declaration", ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", + ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression because it will only be changing a copy of the value" } } @@ -125,7 +128,11 @@ impl Error for ParseError { impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { - ParseErrorType::BadInput(ref s) => write!(f, "{}", s)?, + ParseErrorType::BadInput(ref s) + | ParseErrorType::MalformedIndexExpr(ref s) + | ParseErrorType::MalformedCallExpr(ref s) => { + write!(f, "{}", if s.is_empty() { self.description() } else { s })? + } ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.description(), s)?, ParseErrorType::FnMissingParams(ref s) => { write!(f, "Expecting parameters for function '{}'", s)? @@ -140,6 +147,9 @@ impl fmt::Display for ParseError { if !self.1.is_eof() { write!(f, " ({})", self.1) + } else if !self.1.is_none() { + // Do not write any position if None + Ok(()) } else { write!(f, " at the end of the script but there is no more input") } diff --git a/src/fn_register.rs b/src/fn_register.rs index 9e84492f..9af9ab82 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -1,15 +1,17 @@ -use std::any::TypeId; +//! Module which defines the function registration mechanism. use crate::any::{Any, Dynamic}; use crate::engine::{Engine, FnCallArgs}; use crate::parser::Position; use crate::result::EvalAltResult; +use std::any::TypeId; /// A trait to register custom functions with the `Engine`. /// /// # Example /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, RegisterFn}; /// /// // Normal function @@ -22,9 +24,11 @@ use crate::result::EvalAltResult; /// // You must use the trait rhai::RegisterFn to get this method. /// engine.register_fn("add", add); /// -/// if let Ok(result) = engine.eval::("add(40, 2)") { -/// println!("Answer: {}", result); // prints 42 -/// } +/// let result = engine.eval::("add(40, 2)")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` pub trait RegisterFn { /// Register a custom function with the `Engine`. @@ -36,7 +40,8 @@ pub trait RegisterFn { /// # Example /// /// ```rust -/// use rhai::{Engine, RegisterDynamicFn, Dynamic}; +/// # fn main() -> Result<(), rhai::EvalAltResult> { +/// use rhai::{Engine, Dynamic, RegisterDynamicFn}; /// /// // Function that returns a Dynamic value /// fn get_an_any(x: i64) -> Dynamic { @@ -48,15 +53,46 @@ pub trait RegisterFn { /// // You must use the trait rhai::RegisterDynamicFn to get this method. /// engine.register_dynamic_fn("get_an_any", get_an_any); /// -/// if let Ok(result) = engine.eval::("get_an_any(42)") { -/// println!("Answer: {}", result); // prints 42 -/// } +/// let result = engine.eval::("get_an_any(42)")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } /// ``` pub trait RegisterDynamicFn { /// Register a custom function returning `Dynamic` values with the `Engine`. fn register_dynamic_fn(&mut self, name: &str, f: FN); } +/// A trait to register fallible custom functions returning Result<_, EvalAltResult> with the `Engine`. +/// +/// # Example +/// +/// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { +/// use rhai::{Engine, RegisterFn}; +/// +/// // Normal function +/// fn add(x: i64, y: i64) -> i64 { +/// x + y +/// } +/// +/// let mut engine = Engine::new(); +/// +/// // You must use the trait rhai::RegisterFn to get this method. +/// engine.register_fn("add", add); +/// +/// let result = engine.eval::("add(40, 2)")?; +/// +/// println!("Answer: {}", result); // prints 42 +/// # Ok(()) +/// # } +/// ``` +pub trait RegisterResultFn { + /// Register a custom function with the `Engine`. + fn register_result_fn(&mut self, name: &str, f: FN); +} + pub struct Ref(A); pub struct Mut(A); @@ -90,7 +126,7 @@ macro_rules! def_register { let mut drain = args.drain(..); $( // Downcast every element, return in case of a type mismatch - let $par = (drain.next().unwrap().downcast_mut() as Option<&mut $par>).unwrap(); + let $par = drain.next().unwrap().downcast_mut::<$par>().unwrap(); )* // Call the user-supplied function using ($clone) to @@ -122,7 +158,7 @@ macro_rules! def_register { let mut drain = args.drain(..); $( // Downcast every element, return in case of a type mismatch - let $par = (drain.next().unwrap().downcast_mut() as Option<&mut $par>).unwrap(); + let $par = drain.next().unwrap().downcast_mut::<$par>().unwrap(); )* // Call the user-supplied function using ($clone) to @@ -134,6 +170,44 @@ macro_rules! def_register { } } + impl< + $($par: Any + Clone,)* + FN: Fn($($param),*) -> Result + 'static, + RET: Any + > RegisterResultFn for Engine<'_> + { + fn register_result_fn(&mut self, name: &str, f: FN) { + let fn_name = name.to_string(); + + let fun = move |mut args: FnCallArgs, pos: Position| { + // Check for length at the beginning to avoid per-element bound checks. + const NUM_ARGS: usize = count_args!($($par)*); + + if args.len() != NUM_ARGS { + Err(EvalAltResult::ErrorFunctionArgsMismatch(fn_name.clone(), NUM_ARGS, args.len(), pos)) + } else { + #[allow(unused_variables, unused_mut)] + let mut drain = args.drain(..); + $( + // Downcast every element, return in case of a type mismatch + let $par = drain.next().unwrap().downcast_mut::<$par>().unwrap(); + )* + + // Call the user-supplied function using ($clone) to + // potentially clone the value, otherwise pass the reference. + match f($(($clone)($par)),*) { + Ok(r) => Ok(Box::new(r) as Dynamic), + Err(mut err) => { + err.set_position(pos); + Err(err) + } + } + } + }; + self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun)); + } + } + //def_register!(imp_pop $($par => $mark => $param),*); }; ($p0:ident $(, $p:ident)*) => { diff --git a/src/lib.rs b/src/lib.rs index 5c947e95..56df1ae2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,15 +17,22 @@ //! And the Rust part: //! //! ```rust,no_run -//! use rhai::{Engine, RegisterFn}; +//! use rhai::{Engine, EvalAltResult, RegisterFn}; //! -//! fn compute_something(x: i64) -> bool { -//! (x % 40) == 0 +//! fn main() -> Result<(), EvalAltResult> +//! { +//! fn compute_something(x: i64) -> bool { +//! (x % 40) == 0 +//! } +//! +//! let mut engine = Engine::new(); +//! +//! engine.register_fn("compute_something", compute_something); +//! +//! assert_eq!(engine.eval_file::("my_script.rhai")?, true); +//! +//! Ok(()) //! } -//! -//! let mut engine = Engine::new(); -//! engine.register_fn("compute_something", compute_something); -//! assert_eq!(engine.eval_file::("my_script.rhai").unwrap(), true); //! ``` //! //! [Check out the README on GitHub for more information!](https://github.com/jonathandturner/rhai) @@ -61,6 +68,7 @@ mod call; mod engine; mod error; mod fn_register; +mod optimize; mod parser; mod result; mod scope; @@ -69,7 +77,7 @@ pub use any::{Any, AnyExt, Dynamic, Variant}; pub use call::FuncArgs; pub use engine::{Array, Engine}; pub use error::{ParseError, ParseErrorType}; -pub use fn_register::{RegisterDynamicFn, RegisterFn}; +pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; pub use parser::{Position, AST}; pub use result::EvalAltResult; pub use scope::Scope; diff --git a/src/optimize.rs b/src/optimize.rs new file mode 100644 index 00000000..4a0b26b1 --- /dev/null +++ b/src/optimize.rs @@ -0,0 +1,228 @@ +use crate::parser::{Expr, Stmt}; + +fn optimize_stmt(stmt: Stmt, changed: &mut bool) -> Stmt { + match stmt { + Stmt::IfElse(expr, stmt1, None) => match *expr { + Expr::False(pos) => { + *changed = true; + Stmt::Noop(pos) + } + Expr::True(_) => optimize_stmt(*stmt1, changed), + expr => Stmt::IfElse( + Box::new(optimize_expr(expr, changed)), + Box::new(optimize_stmt(*stmt1, changed)), + None, + ), + }, + + Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr { + Expr::False(_) => optimize_stmt(*stmt2, changed), + Expr::True(_) => optimize_stmt(*stmt1, changed), + expr => Stmt::IfElse( + Box::new(optimize_expr(expr, changed)), + Box::new(optimize_stmt(*stmt1, changed)), + Some(Box::new(optimize_stmt(*stmt2, changed))), + ), + }, + + Stmt::While(expr, stmt) => match *expr { + Expr::False(pos) => { + *changed = true; + Stmt::Noop(pos) + } + Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed))), + expr => Stmt::While( + Box::new(optimize_expr(expr, changed)), + Box::new(optimize_stmt(*stmt, changed)), + ), + }, + + Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed))), + Stmt::For(id, expr, stmt) => Stmt::For( + id, + Box::new(optimize_expr(*expr, changed)), + Box::new(optimize_stmt(*stmt, changed)), + ), + Stmt::Let(id, Some(expr), pos) => { + Stmt::Let(id, Some(Box::new(optimize_expr(*expr, changed))), pos) + } + Stmt::Let(_, None, _) => stmt, + + Stmt::Block(statements, pos) => { + let original_len = statements.len(); + + let mut result: Vec<_> = statements + .into_iter() // For each statement + .map(|s| optimize_stmt(s, changed)) // Optimize the statement + .filter(Stmt::is_op) // Remove no-op's + .collect(); + + if let Some(last_stmt) = result.pop() { + // Remove all raw expression statements that evaluate to constants + // except for the very last statement + result.retain(|stmt| match stmt { + Stmt::Expr(expr) if expr.is_constant() => false, + _ => true, + }); + + result.push(last_stmt); + } + + *changed = *changed || original_len != result.len(); + + match result[..] { + [] => { + // No statements in block - change to No-op + *changed = true; + Stmt::Noop(pos) + } + [Stmt::Let(_, None, _)] => { + // Only one empty variable declaration - change to No-op + *changed = true; + Stmt::Noop(pos) + } + [Stmt::Let(_, Some(_), _)] => { + // Only one let statement, but cannot promote + // (otherwise the variable gets declared in the scope above) + // and still need to run just in case there are side effects + Stmt::Block(result, pos) + } + [_] => { + // Only one statement - promote + *changed = true; + result.remove(0) + } + _ => Stmt::Block(result, pos), + } + } + + Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, changed))), + + Stmt::ReturnWithVal(Some(expr), is_return, pos) => Stmt::ReturnWithVal( + Some(Box::new(optimize_expr(*expr, changed))), + is_return, + pos, + ), + stmt @ Stmt::ReturnWithVal(None, _, _) => stmt, + + stmt @ Stmt::Noop(_) | stmt @ Stmt::Break(_) => stmt, + } +} + +fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr { + match expr { + Expr::IntegerConstant(_, _) + | Expr::FloatConstant(_, _) + | Expr::Identifier(_, _) + | Expr::CharConstant(_, _) + | Expr::StringConstant(_, _) + | Expr::True(_) + | Expr::False(_) + | Expr::Unit(_) => expr, + + Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, changed) { + Stmt::Noop(_) => { + *changed = true; + Expr::Unit(pos) + } + Stmt::Expr(expr) => { + *changed = true; + *expr + } + stmt => Expr::Stmt(Box::new(stmt), pos), + }, + Expr::Assignment(id, expr, pos) => { + Expr::Assignment(id, Box::new(optimize_expr(*expr, changed)), pos) + } + Expr::Dot(lhs, rhs, pos) => Expr::Dot( + Box::new(optimize_expr(*lhs, changed)), + Box::new(optimize_expr(*rhs, changed)), + pos, + ), + Expr::Index(lhs, rhs, pos) => Expr::Index( + Box::new(optimize_expr(*lhs, changed)), + Box::new(optimize_expr(*rhs, changed)), + pos, + ), + Expr::Array(items, pos) => { + let original_len = items.len(); + + let items: Vec<_> = items + .into_iter() + .map(|expr| optimize_expr(expr, changed)) + .collect(); + + *changed = *changed || original_len != items.len(); + + Expr::Array(items, pos) + } + + Expr::And(lhs, rhs) => match (*lhs, *rhs) { + (Expr::True(_), rhs) => { + *changed = true; + rhs + } + (Expr::False(pos), _) => { + *changed = true; + Expr::False(pos) + } + (lhs, Expr::True(_)) => { + *changed = true; + lhs + } + (lhs, rhs) => Expr::And( + Box::new(optimize_expr(lhs, changed)), + Box::new(optimize_expr(rhs, changed)), + ), + }, + Expr::Or(lhs, rhs) => match (*lhs, *rhs) { + (Expr::False(_), rhs) => { + *changed = true; + rhs + } + (Expr::True(pos), _) => { + *changed = true; + Expr::True(pos) + } + (lhs, Expr::False(_)) => { + *changed = true; + lhs + } + (lhs, rhs) => Expr::Or( + Box::new(optimize_expr(lhs, changed)), + Box::new(optimize_expr(rhs, changed)), + ), + }, + + Expr::FunctionCall(id, args, def_value, pos) => { + let original_len = args.len(); + + let args: Vec<_> = args + .into_iter() + .map(|a| optimize_expr(a, changed)) + .collect(); + + *changed = *changed || original_len != args.len(); + + Expr::FunctionCall(id, args, def_value, pos) + } + } +} + +pub(crate) fn optimize(mut statements: Vec) -> Vec { + loop { + let mut changed = false; + + statements = statements + .into_iter() + .map(|stmt| optimize_stmt(stmt, &mut changed)) + .filter(Stmt::is_op) + .collect(); + + if !changed { + break; + } + } + + statements +} diff --git a/src/parser.rs b/src/parser.rs index 204998b0..10e629d0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,8 +1,9 @@ +//! Main module defining the lexer and parser. + use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use std::char; -use std::iter::Peekable; -use std::str::Chars; +use crate::optimize::optimize; +use std::{borrow::Cow, char, fmt, iter::Peekable, str::Chars, str::FromStr, usize}; type LERR = LexError; type PERR = ParseErrorType; @@ -17,25 +18,33 @@ pub struct Position { impl Position { /// Create a new `Position`. pub fn new(line: usize, position: usize) -> Self { + if line == 0 || (line == usize::MAX && position == usize::MAX) { + panic!("invalid position: ({}, {})", line, position); + } + Self { line, pos: position, } } - /// Get the line number (1-based), or `None` if EOF. + /// Get the line number (1-based), or `None` if no position or EOF. pub fn line(&self) -> Option { - match self.line { - 0 => None, - x => Some(x), + if self.is_none() || self.is_eof() { + None + } else { + Some(self.line) } } /// Get the character position (1-based), or `None` if at beginning of a line. pub fn position(&self) -> Option { - match self.pos { - 0 => None, - x => Some(x), + if self.is_none() || self.is_eof() { + None + } else if self.pos == 0 { + None + } else { + Some(self.pos) } } @@ -61,14 +70,27 @@ impl Position { self.pos = 0; } + /// Create a `Position` representing no position. + pub(crate) fn none() -> Self { + Self { line: 0, pos: 0 } + } + /// Create a `Position` at EOF. pub(crate) fn eof() -> Self { - Self { line: 0, pos: 0 } + Self { + line: usize::MAX, + pos: usize::MAX, + } + } + + /// Is there no `Position`? + pub fn is_none(&self) -> bool { + self.line == 0 && self.pos == 0 } /// Is the `Position` at EOF? pub fn is_eof(&self) -> bool { - self.line == 0 + self.line == usize::MAX && self.pos == usize::MAX } } @@ -78,18 +100,20 @@ impl Default for Position { } } -impl std::fmt::Display for Position { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_eof() { write!(f, "EOF") + } else if self.is_none() { + write!(f, "none") } else { write!(f, "line {}, position {}", self.line, self.pos) } } } -impl std::fmt::Debug for Position { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_eof() { write!(f, "(EOF)") } else { @@ -99,29 +123,39 @@ impl std::fmt::Debug for Position { } /// Compiled AST (abstract syntax tree) of a Rhai script. -pub struct AST(pub(crate) Vec, pub(crate) Vec); +pub struct AST(pub(crate) Vec, pub(crate) Vec>); #[derive(Debug, Clone)] -pub struct FnDef { - pub name: String, - pub params: Vec, - pub body: Box, +pub struct FnDef<'a> { + pub name: Cow<'a, str>, + pub params: Vec>, + pub body: Stmt, pub pos: Position, } #[derive(Debug, Clone)] pub enum Stmt { + Noop(Position), IfElse(Box, Box, Option>), While(Box, Box), Loop(Box), For(String, Box, Box), Let(String, Option>, Position), - Block(Vec), + Block(Vec, Position), Expr(Box), Break(Position), ReturnWithVal(Option>, bool, Position), } +impl Stmt { + pub fn is_op(&self) -> bool { + match self { + Stmt::Noop(_) => false, + _ => true, + } + } +} + #[derive(Debug, Clone)] pub enum Expr { IntegerConstant(i64, Position), @@ -129,10 +163,11 @@ pub enum Expr { Identifier(String, Position), CharConstant(char, Position), StringConstant(String, Position), + Stmt(Box, Position), FunctionCall(String, Vec, Option, Position), - Assignment(Box, Box), - Dot(Box, Box), - Index(Box, Box), + Assignment(Box, Box, Position), + Dot(Box, Box, Position), + Index(Box, Box, Position), Array(Vec, Position), And(Box, Box), Or(Box, Box), @@ -150,18 +185,34 @@ impl Expr { | Expr::CharConstant(_, pos) | Expr::StringConstant(_, pos) | Expr::FunctionCall(_, _, _, pos) + | Expr::Stmt(_, pos) | Expr::Array(_, pos) | Expr::True(pos) | Expr::False(pos) | Expr::Unit(pos) => *pos, - Expr::Index(e, _) - | Expr::Assignment(e, _) - | Expr::Dot(e, _) + Expr::Index(e, _, _) + | Expr::Assignment(e, _, _) + | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => e.position(), } } + + pub fn is_constant(&self) -> bool { + match self { + Expr::IntegerConstant(_, _) + | Expr::FloatConstant(_, _) + | Expr::Identifier(_, _) + | Expr::CharConstant(_, _) + | Expr::StringConstant(_, _) + | Expr::True(_) + | Expr::False(_) + | Expr::Unit(_) => true, + + _ => false, + } + } } #[derive(Debug, PartialEq, Clone)] @@ -232,14 +283,14 @@ pub enum Token { } impl Token { - pub fn syntax(&self) -> std::borrow::Cow<'static, str> { + pub fn syntax<'a>(&'a self) -> Cow<'a, str> { use self::Token::*; match *self { - IntegerConstant(ref s) => s.to_string().into(), - FloatConstant(ref s) => s.to_string().into(), - Identifier(ref s) => s.to_string().into(), - CharConstant(ref s) => s.to_string().into(), + IntegerConstant(ref i) => i.to_string().into(), + FloatConstant(ref f) => f.to_string().into(), + Identifier(ref s) => s.into(), + CharConstant(ref c) => c.to_string().into(), LexError(ref err) => err.to_string().into(), ref token => (match token { @@ -301,7 +352,7 @@ impl Token { PowerOfAssign => "~=", For => "for", In => "in", - _ => panic!(), + _ => panic!("operator should be match in outer scope"), }) .into(), } @@ -313,6 +364,7 @@ impl Token { use self::Token::*; match *self { + LexError(_) | LeftBrace | // (+expr) - is unary // RightBrace | {expr} - expr not unary & is closing LeftParen | // {-expr} - is unary @@ -361,6 +413,7 @@ impl Token { PowerOf | In | PowerOfAssign => true, + _ => false, } } @@ -373,6 +426,7 @@ impl Token { RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true, + _ => false, } } @@ -538,11 +592,12 @@ impl<'a> TokenIterator<'a> { } } - let out: String = result.iter().collect(); - Ok(out) + Ok(result.iter().collect()) } fn inner_next(&mut self) -> Option<(Token, Position)> { + let mut negated = false; + while let Some(c) = self.char_stream.next() { self.advance(); @@ -629,31 +684,34 @@ impl<'a> TokenIterator<'a> { } } + if negated { + result.insert(0, '-'); + } + if let Some(radix) = radix_base { let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); return Some(( - if let Ok(val) = i64::from_str_radix(&out, radix) { - Token::IntegerConstant(val) - } else { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }, + i64::from_str_radix(&out, radix) + .map(Token::IntegerConstant) + .unwrap_or_else(|_| { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }), + pos, + )); + } else { + let out: String = result.iter().filter(|&&c| c != '_').collect(); + + return Some(( + i64::from_str(&out) + .map(Token::IntegerConstant) + .or_else(|_| f64::from_str(&out).map(Token::FloatConstant)) + .unwrap_or_else(|_| { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }), pos, )); } - - let out: String = result.iter().filter(|&&c| c != '_').collect(); - - return Some(( - if let Ok(val) = out.parse::() { - Token::IntegerConstant(val) - } else if let Ok(val) = out.parse::() { - Token::FloatConstant(val) - } else { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }, - pos, - )); } 'A'..='Z' | 'a'..='z' | '_' => { let mut result = Vec::new(); @@ -687,7 +745,7 @@ impl<'a> TokenIterator<'a> { "fn" => Token::Fn, "for" => Token::For, "in" => Token::In, - x => Token::Identifier(x.into()), + _ => Token::Identifier(out), }, pos, )); @@ -737,20 +795,17 @@ impl<'a> TokenIterator<'a> { pos, )) } - '-' => { - return Some(( - match self.char_stream.peek() { - Some(&'=') => { - self.char_stream.next(); - self.advance(); - Token::MinusAssign - } - _ if self.last.is_next_unary() => Token::UnaryMinus, - _ => Token::Minus, - }, - pos, - )) - } + '-' => match self.char_stream.peek() { + // Negative number? + Some('0'..='9') => negated = true, + Some('=') => { + self.char_stream.next(); + self.advance(); + return Some((Token::MinusAssign, pos)); + } + _ if self.last.is_next_unary() => return Some((Token::UnaryMinus, pos)), + _ => return Some((Token::Minus, pos)), + }, '*' => { return Some(( match self.char_stream.peek() { @@ -1010,19 +1065,28 @@ fn get_precedence(token: &Token) -> i8 { | Token::XOrAssign | Token::ModuloAssign | Token::PowerOfAssign => 10, + Token::Or | Token::XOr | Token::Pipe => 11, + Token::And | Token::Ampersand => 12, + Token::LessThan | Token::LessThanEqualsTo | Token::GreaterThan | Token::GreaterThanEqualsTo | Token::EqualsTo | Token::NotEqualsTo => 15, + Token::Plus | Token::Minus => 20, + Token::Divide | Token::Multiply | Token::PowerOf => 40, + Token::LeftShift | Token::RightShift => 50, + Token::Modulo => 60, + Token::Period => 100, + _ => -1, } } @@ -1032,6 +1096,7 @@ fn parse_paren_expr<'a>( begin: Position, ) -> Result { match input.peek() { + // () Some((Token::RightParen, _)) => { input.next(); return Ok(Expr::Unit(begin)); @@ -1104,11 +1169,71 @@ fn parse_call_expr<'a>( fn parse_index_expr<'a>( lhs: Box, input: &mut Peekable>, + pos: Position, ) -> Result { - parse_expr(input).and_then(|idx_expr| match input.peek() { + let idx_expr = parse_expr(input)?; + + // Check type of indexing - must be integer + match &idx_expr { + Expr::IntegerConstant(i, pos) if *i < 0 => { + return Err(ParseError::new( + PERR::MalformedIndexExpr(format!( + "Array access expects non-negative index: {} < 0", + i + )), + *pos, + )) + } + Expr::FloatConstant(_, pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr("Array access expects integer index, not a float".into()), + *pos, + )) + } + Expr::CharConstant(_, pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr( + "Array access expects integer index, not a character".into(), + ), + *pos, + )) + } + Expr::StringConstant(_, pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr("Array access expects integer index, not a string".into()), + *pos, + )) + } + Expr::Assignment(_, _, pos) | Expr::Unit(pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr("Array access expects integer index, not ()".into()), + *pos, + )) + } + Expr::And(lhs, _) | Expr::Or(lhs, _) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr( + "Array access expects integer index, not a boolean".into(), + ), + lhs.position(), + )) + } + Expr::True(pos) | Expr::False(pos) => { + return Err(ParseError::new( + PERR::MalformedIndexExpr( + "Array access expects integer index, not a boolean".into(), + ), + *pos, + )) + } + _ => (), + } + + // Check if there is a closing bracket + match input.peek() { Some(&(Token::RightBracket, _)) => { input.next(); - return Ok(Expr::Index(lhs, Box::new(idx_expr))); + return Ok(Expr::Index(lhs, Box::new(idx_expr), pos)); } Some(&(_, pos)) => { return Err(ParseError::new( @@ -1122,7 +1247,7 @@ fn parse_index_expr<'a>( Position::eof(), )) } - }) + } } fn parse_ident_expr<'a>( @@ -1135,9 +1260,9 @@ fn parse_ident_expr<'a>( input.next(); parse_call_expr(id, input, begin) } - Some(&(Token::LeftBracket, _)) => { + Some(&(Token::LeftBracket, pos)) => { input.next(); - parse_index_expr(Box::new(Expr::Identifier(id, begin)), input) + parse_index_expr(Box::new(Expr::Identifier(id, begin)), input, pos) } Some(_) => Ok(Expr::Identifier(id, begin)), None => Ok(Expr::Identifier(id, Position::eof())), @@ -1185,6 +1310,14 @@ fn parse_array_expr<'a>( } fn parse_primary<'a>(input: &mut Peekable>) -> Result { + // Block statement as expression + match input.peek() { + Some(&(Token::LeftBrace, pos)) => { + return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)) + } + _ => (), + } + let token = input.next(); let mut follow_on = false; @@ -1226,9 +1359,9 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); - Ok(Expr::FunctionCall( - "-".into(), - vec![parse_primary(input)?], - None, - pos, - )) + match parse_unary(input) { + // Negative integer + Ok(Expr::IntegerConstant(i, pos)) => Ok(i + .checked_neg() + .map(|x| Expr::IntegerConstant(x, pos)) + .unwrap_or_else(|| Expr::FloatConstant(-(i as f64), pos))), + // Negative float + Ok(Expr::FloatConstant(f, pos)) => Ok(Expr::FloatConstant(-f, pos)), + // Call negative function + Ok(expr) => Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), + err @ Err(_) => err, + } } Some(&(Token::UnaryPlus, _)) => { input.next(); - parse_primary(input) + parse_unary(input) } Some(&(Token::Bang, pos)) => { input.next(); @@ -1264,6 +1403,75 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Result { + fn valid_assignment_chain(expr: &Expr) -> (bool, Position) { + match expr { + Expr::Identifier(_, pos) => (true, *pos), + + Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { + Expr::Identifier(_, _) => (true, idx_lhs.position()), + _ => (false, idx_lhs.position()), + }, + + Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { + Expr::Identifier(_, _) => valid_assignment_chain(dot_rhs), + Expr::Index(idx_lhs, _, _) => match idx_lhs.as_ref() { + Expr::Identifier(_, _) => valid_assignment_chain(dot_rhs), + _ => (false, idx_lhs.position()), + }, + _ => (false, dot_lhs.position()), + }, + + _ => (false, expr.position()), + } + } + + //println!("{:?} = {:?}", lhs, rhs); + + match valid_assignment_chain(&lhs) { + (true, _) => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), + (false, pos) => Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), + } +} + +fn parse_op_assignment( + function: &str, + lhs: Expr, + rhs: Expr, + pos: Position, +) -> Result { + let lhs_copy = lhs.clone(); + + parse_assignment( + lhs, + Expr::FunctionCall(function.into(), vec![lhs_copy, rhs], None, pos), + pos, + ) + + /* + const LHS_VALUE: &'static str = "@LHS_VALUE@"; + + let lhs_pos = lhs.position(); + + Ok(Expr::Block( + Box::new(Stmt::Block(vec![ + Stmt::Let(LHS_VALUE.to_string(), Some(Box::new(lhs)), lhs_pos), + Stmt::Expr(Box::new(parse_assignment( + lhs, + Expr::FunctionCall( + function.into(), + vec![Expr::Identifier(LHS_VALUE.to_string(), lhs_pos), rhs], + None, + pos, + ), + pos, + )?)), + ])), + pos, + )) + */ +} + fn parse_binary_op<'a>( input: &mut Peekable>, precedence: i8, @@ -1308,32 +1516,10 @@ fn parse_binary_op<'a>( } Token::Divide => Expr::FunctionCall("/".into(), vec![current_lhs, rhs], None, pos), - Token::Equals => Expr::Assignment(Box::new(current_lhs), Box::new(rhs)), - Token::PlusAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "+".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::MinusAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "-".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::Period => Expr::Dot(Box::new(current_lhs), Box::new(rhs)), + Token::Equals => parse_assignment(current_lhs, rhs, pos)?, + Token::PlusAssign => parse_op_assignment("+", current_lhs, rhs, pos)?, + Token::MinusAssign => parse_op_assignment("-", current_lhs, rhs, pos)?, + Token::Period => Expr::Dot(Box::new(current_lhs), Box::new(rhs), pos), // Comparison operators default to false when passed invalid operands Token::EqualsTo => Expr::FunctionCall( @@ -1376,66 +1562,11 @@ fn parse_binary_op<'a>( Token::Or => Expr::Or(Box::new(current_lhs), Box::new(rhs)), Token::And => Expr::And(Box::new(current_lhs), Box::new(rhs)), Token::XOr => Expr::FunctionCall("^".into(), vec![current_lhs, rhs], None, pos), - Token::OrAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "|".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::AndAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "&".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::XOrAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "^".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::MultiplyAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "*".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::DivideAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "/".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } + Token::OrAssign => parse_op_assignment("|", current_lhs, rhs, pos)?, + Token::AndAssign => parse_op_assignment("&", current_lhs, rhs, pos)?, + Token::XOrAssign => parse_op_assignment("^", current_lhs, rhs, pos)?, + Token::MultiplyAssign => parse_op_assignment("*", current_lhs, rhs, pos)?, + Token::DivideAssign => parse_op_assignment("/", current_lhs, rhs, pos)?, Token::Pipe => Expr::FunctionCall("|".into(), vec![current_lhs, rhs], None, pos), Token::LeftShift => { Expr::FunctionCall("<<".into(), vec![current_lhs, rhs], None, pos) @@ -1443,59 +1574,15 @@ fn parse_binary_op<'a>( Token::RightShift => { Expr::FunctionCall(">>".into(), vec![current_lhs, rhs], None, pos) } - Token::LeftShiftAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "<<".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } - Token::RightShiftAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - ">>".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } + Token::LeftShiftAssign => parse_op_assignment("<<", current_lhs, rhs, pos)?, + Token::RightShiftAssign => parse_op_assignment(">>", current_lhs, rhs, pos)?, Token::Ampersand => { Expr::FunctionCall("&".into(), vec![current_lhs, rhs], None, pos) } Token::Modulo => Expr::FunctionCall("%".into(), vec![current_lhs, rhs], None, pos), - Token::ModuloAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "%".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } + Token::ModuloAssign => parse_op_assignment("%", current_lhs, rhs, pos)?, Token::PowerOf => Expr::FunctionCall("~".into(), vec![current_lhs, rhs], None, pos), - Token::PowerOfAssign => { - let lhs_copy = current_lhs.clone(); - Expr::Assignment( - Box::new(current_lhs), - Box::new(Expr::FunctionCall( - "~".into(), - vec![lhs_copy, rhs], - None, - pos, - )), - ) - } + Token::PowerOfAssign => parse_op_assignment("~", current_lhs, rhs, pos)?, token => { return Err(ParseError::new( PERR::UnknownOperator(token.syntax().into()), @@ -1591,8 +1678,8 @@ fn parse_var<'a>(input: &mut Peekable>) -> Result { input.next(); - let initializer = parse_expr(input)?; - Ok(Stmt::Let(name, Some(Box::new(initializer)), pos)) + let init_value = parse_expr(input)?; + Ok(Stmt::Let(name, Some(Box::new(init_value)), pos)) } _ => Ok(Stmt::Let(name, None, pos)), } @@ -1605,7 +1692,7 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::MissingLeftBrace, Position::eof())), } - input.next(); + let pos = input.next().unwrap().1; let mut statements = Vec::new(); @@ -1633,7 +1720,7 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result { input.next(); - Ok(Stmt::Block(statements)) + Ok(Stmt::Block(statements, pos)) } Some(&(_, pos)) => Err(ParseError::new( PERR::MissingRightBrace("end of block".into()), @@ -1687,7 +1774,7 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_fn<'a>(input: &mut Peekable>) -> Result, ParseError> { let pos = match input.next() { Some((_, tok_pos)) => tok_pos, _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), @@ -1723,10 +1810,24 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result break, Some((Token::Comma, _)) => (), Some((Token::Identifier(s), _)) => { - params.push(s); + params.push(s.into()); + } + Some((_, pos)) => { + return Err(ParseError::new( + PERR::MalformedCallExpr( + "Function call arguments missing either a ',' or a ')'".into(), + ), + pos, + )) + } + None => { + return Err(ParseError::new( + PERR::MalformedCallExpr( + "Function call arguments missing a closing ')'".into(), + ), + Position::eof(), + )) } - Some((_, pos)) => return Err(ParseError::new(PERR::MalformedCallExpr, pos)), - None => return Err(ParseError::new(PERR::MalformedCallExpr, Position::eof())), } }, } @@ -1734,14 +1835,17 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_top_level<'a>( + input: &mut Peekable>, + optimize_ast: bool, +) -> Result { let mut statements = Vec::new(); let mut functions = Vec::new(); @@ -1757,9 +1861,26 @@ fn parse_top_level<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { - parse_top_level(input) +pub fn parse<'a>( + input: &mut Peekable>, + optimize_ast: bool, +) -> Result { + parse_top_level(input, optimize_ast) } diff --git a/src/result.rs b/src/result.rs index 1d869c8b..224c8232 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,8 +1,9 @@ -use std::error::Error; +//! Module containing error definitions for the evaluation process. use crate::any::Dynamic; use crate::error::ParseError; use crate::parser::Position; +use std::{error::Error, fmt}; /// Evaluation result. /// @@ -19,6 +20,8 @@ pub enum EvalAltResult { ErrorFunctionArgsMismatch(String, usize, usize, Position), /// Non-boolean operand encountered for boolean operator. Wrapped value is the operator. ErrorBooleanArgMismatch(String, Position), + /// Non-character value encountered where a character is required. + ErrorCharMismatch(Position), /// Array access out-of-bounds. /// Wrapped values are the current number of elements in the array and the index number. ErrorArrayBounds(usize, i64, Position), @@ -26,7 +29,7 @@ pub enum EvalAltResult { /// Wrapped values are the current number of characters in the string and the index number. ErrorStringBounds(usize, i64, Position), /// Trying to index into a type that is not an array and not a string. - ErrorIndexingType(Position), + ErrorIndexingType(String, Position), /// Trying to index into an array or string with an index that is not `i64`. ErrorIndexExpr(Position), /// The guard expression in an `if` statement does not return a boolean value. @@ -43,7 +46,7 @@ pub enum EvalAltResult { /// Error reading from a script file. Wrapped value is the path of the script file. ErrorReadingScriptFile(String, std::io::Error), /// Inappropriate member access. - ErrorDotExpr(Position), + ErrorDotExpr(String, Position), /// Arithmetic error encountered. Wrapped value is the error message. ErrorArithmetic(String, Position), /// Run-time error encountered. Wrapped value is the error message. @@ -64,8 +67,11 @@ impl Error for EvalAltResult { "Function call with wrong number of arguments" } Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", + Self::ErrorCharMismatch(_) => "Character expected", Self::ErrorIndexExpr(_) => "Indexing into an array or string expects an integer index", - Self::ErrorIndexingType(_) => "Indexing can only be performed on an array or a string", + Self::ErrorIndexingType(_, _) => { + "Indexing can only be performed on an array or a string" + } Self::ErrorArrayBounds(_, index, _) if *index < 0 => { "Array access expects non-negative index" } @@ -84,7 +90,7 @@ impl Error for EvalAltResult { } Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect", Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file", - Self::ErrorDotExpr(_) => "Malformed dot expression", + Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorRuntime(_, _) => "Runtime error", Self::LoopBreak => "[Not Error] Breaks out of loop", @@ -97,23 +103,25 @@ impl Error for EvalAltResult { } } -impl std::fmt::Display for EvalAltResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for EvalAltResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let desc = self.description(); 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::ErrorIndexingType(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorIndexExpr(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), - Self::ErrorDotExpr(pos) => write!(f, "{} ({})", desc, pos), - Self::ErrorArithmetic(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), - Self::ErrorRuntime(s, pos) if s.is_empty() => write!(f, "{} ({})", desc, pos), - Self::ErrorRuntime(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::ErrorRuntime(s, pos) => { + write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) + } Self::LoopBreak => write!(f, "{}", desc), Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorReadingScriptFile(filename, err) => { @@ -128,6 +136,9 @@ impl std::fmt::Display for EvalAltResult { Self::ErrorBooleanArgMismatch(op, pos) => { write!(f, "{} operator expects boolean operands ({})", op, pos) } + Self::ErrorCharMismatch(pos) => { + write!(f, "string indexing expects a character value ({})", pos) + } Self::ErrorArrayBounds(_, index, pos) if *index < 0 => { write!(f, "{}: {} < 0 ({})", desc, index, pos) } @@ -161,3 +172,62 @@ impl From for EvalAltResult { Self::ErrorParsing(err) } } + +impl EvalAltResult { + pub fn position(&self) -> Position { + match self { + Self::ErrorReadingScriptFile(_, _) | Self::LoopBreak => Position::none(), + + Self::ErrorParsing(err) => err.position(), + + Self::ErrorFunctionNotFound(_, pos) + | Self::ErrorFunctionArgsMismatch(_, _, _, pos) + | Self::ErrorBooleanArgMismatch(_, pos) + | Self::ErrorCharMismatch(pos) + | Self::ErrorArrayBounds(_, _, pos) + | Self::ErrorStringBounds(_, _, pos) + | Self::ErrorIndexingType(_, pos) + | Self::ErrorIndexExpr(pos) + | Self::ErrorIfGuard(pos) + | Self::ErrorFor(pos) + | Self::ErrorVariableNotFound(_, pos) + | Self::ErrorAssignmentToUnknownLHS(pos) + | Self::ErrorMismatchOutputType(_, pos) + | Self::ErrorDotExpr(_, pos) + | Self::ErrorArithmetic(_, pos) + | Self::ErrorRuntime(_, pos) + | Self::Return(_, pos) => *pos, + } + } + + pub(crate) fn set_position(&mut self, new_position: Position) { + match self { + Self::ErrorReadingScriptFile(_, _) | Self::LoopBreak => (), + + Self::ErrorParsing(ParseError(_, ref mut pos)) + | Self::ErrorFunctionNotFound(_, ref mut pos) + | Self::ErrorFunctionArgsMismatch(_, _, _, ref mut pos) + | Self::ErrorBooleanArgMismatch(_, ref mut pos) + | Self::ErrorCharMismatch(ref mut pos) + | Self::ErrorArrayBounds(_, _, ref mut pos) + | Self::ErrorStringBounds(_, _, ref mut pos) + | Self::ErrorIndexingType(_, ref mut pos) + | Self::ErrorIndexExpr(ref mut pos) + | Self::ErrorIfGuard(ref mut pos) + | Self::ErrorFor(ref mut pos) + | Self::ErrorVariableNotFound(_, ref mut pos) + | Self::ErrorAssignmentToUnknownLHS(ref mut pos) + | Self::ErrorMismatchOutputType(_, ref mut pos) + | Self::ErrorDotExpr(_, ref mut pos) + | Self::ErrorArithmetic(_, ref mut pos) + | Self::ErrorRuntime(_, ref mut pos) + | Self::Return(_, ref mut pos) => *pos = new_position, + } + } +} + +impl> From for EvalAltResult { + fn from(err: T) -> Self { + Self::ErrorRuntime(err.as_ref().to_string(), Position::none()) + } +} diff --git a/src/scope.rs b/src/scope.rs index 9e7f63dc..2e2c492b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,25 +1,32 @@ +//! Module that defines the `Scope` type representing a function call-stack scope. + use crate::any::{Any, Dynamic}; +use std::borrow::Cow; /// A type containing information about current scope. -/// Useful for keeping state between `Engine` runs +/// Useful for keeping state between `Engine` runs. /// /// # Example /// /// ```rust +/// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// /// let mut engine = Engine::new(); /// let mut my_scope = Scope::new(); /// -/// assert!(engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;").is_ok()); -/// assert_eq!(engine.eval_with_scope::(&mut my_scope, "x + 1").unwrap(), 6); +/// engine.eval_with_scope::<()>(&mut my_scope, false, "let x = 5;")?; +/// +/// assert_eq!(engine.eval_with_scope::(&mut my_scope, false, "x + 1")?, 6); +/// # Ok(()) +/// # } /// ``` /// /// When searching for variables, newly-added variables are found before similarly-named but older variables, /// allowing for automatic _shadowing_ of variables. -pub struct Scope(Vec<(String, Dynamic)>); +pub struct Scope<'a>(Vec<(Cow<'a, str>, Dynamic)>); -impl Scope { +impl<'a> Scope<'a> { /// Create a new Scope. pub fn new() -> Self { Self(Vec::new()) @@ -36,18 +43,18 @@ impl Scope { } /// Add (push) a new variable to the Scope. - pub fn push(&mut self, key: String, value: T) { - self.0.push((key, Box::new(value))); + pub fn push>, T: Any>(&mut self, key: K, value: T) { + self.0.push((key.into(), Box::new(value))); } /// Add (push) a new variable to the Scope. - pub(crate) fn push_dynamic(&mut self, key: String, value: Dynamic) { - self.0.push((key, value)); + pub(crate) fn push_dynamic>>(&mut self, key: K, value: Dynamic) { + self.0.push((key.into(), value)); } /// Remove (pop) the last variable from the Scope. pub fn pop(&mut self) -> Option<(String, Dynamic)> { - self.0.pop() + self.0.pop().map(|(key, value)| (key.to_string(), value)) } /// Truncate (rewind) the Scope to a previous size. @@ -56,13 +63,13 @@ impl Scope { } /// Find a variable in the Scope, starting from the last. - pub fn get(&self, key: &str) -> Option<(usize, String, Dynamic)> { + pub fn get(&self, key: &str) -> Option<(usize, &str, Dynamic)> { self.0 .iter() .enumerate() .rev() // Always search a Scope in reverse order .find(|(_, (name, _))| name == key) - .map(|(i, (name, value))| (i, name.clone(), value.clone())) + .map(|(i, (name, value))| (i, name.as_ref(), value.clone())) } /// Get the value of a variable in the Scope, starting from the last. @@ -97,20 +104,26 @@ impl Scope { self.0 .iter() .rev() // Always search a Scope in reverse order - .map(|(key, value)| (key.as_str(), value)) + .map(|(key, value)| (key.as_ref(), value)) } + /* /// Get a mutable iterator to variables in the Scope. pub fn iter_mut(&mut self) -> impl Iterator { self.0 .iter_mut() .rev() // Always search a Scope in reverse order - .map(|(key, value)| (key.as_str(), value)) + .map(|(key, value)| (key.as_ref(), value)) } + */ } -impl std::iter::Extend<(String, Dynamic)> for Scope { - fn extend>(&mut self, iter: T) { - self.0.extend(iter); +impl<'a, K> std::iter::Extend<(K, Dynamic)> for Scope<'a> +where + K: Into>, +{ + fn extend>(&mut self, iter: T) { + self.0 + .extend(iter.into_iter().map(|(key, value)| (key.into(), value))); } } diff --git a/tests/engine.rs b/tests/engine.rs index 082daaa4..b55a7c44 100644 --- a/tests/engine.rs +++ b/tests/engine.rs @@ -4,9 +4,9 @@ use rhai::{Engine, EvalAltResult}; fn test_engine_call_fn() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - let ast = Engine::compile("fn hello(x, y) { x.len() + y }")?; + let ast = engine.compile("fn hello(x, y) { x.len() + y }")?; - let result: i64 = engine.call_fn("hello", ast, (&mut String::from("abc"), &mut 123_i64))?; + let result: i64 = engine.call_fn("hello", &ast, (&mut String::from("abc"), &mut 123_i64))?; assert_eq!(result, 126); diff --git a/tests/math.rs b/tests/math.rs new file mode 100644 index 00000000..9b92f1fb --- /dev/null +++ b/tests/math.rs @@ -0,0 +1,43 @@ +use rhai::{Engine, EvalAltResult}; + +#[test] +fn test_math() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + assert_eq!(engine.eval::("1 + 2")?, 3); + assert_eq!(engine.eval::("1 - 2")?, -1); + assert_eq!(engine.eval::("2 * 3")?, 6); + assert_eq!(engine.eval::("1 / 2")?, 0); + assert_eq!(engine.eval::("3 % 2")?, 1); + assert_eq!( + engine.eval::("(-9223372036854775807).abs()")?, + 9223372036854775807 + ); + + // Overflow/underflow/division-by-zero errors + #[cfg(not(feature = "unchecked"))] + { + match engine.eval::("9223372036854775807 + 1") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return overflow error: {:?}", r), + } + match engine.eval::("-9223372036854775808 - 1") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return underflow error: {:?}", r), + } + match engine.eval::("9223372036854775807 * 9223372036854775807") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return overflow error: {:?}", r), + } + match engine.eval::("9223372036854775807 / 0") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return division by zero error: {:?}", r), + } + match engine.eval::("9223372036854775807 % 0") { + Err(EvalAltResult::ErrorArithmetic(_, _)) => (), + r => panic!("should return division by zero error: {:?}", r), + } + } + + Ok(()) +} diff --git a/tests/unary_minus.rs b/tests/unary_minus.rs index b32e6860..37718326 100644 --- a/tests/unary_minus.rs +++ b/tests/unary_minus.rs @@ -5,8 +5,8 @@ fn test_unary_minus() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); assert_eq!(engine.eval::("let x = -5; x")?, -5); - assert_eq!(engine.eval::("fn n(x) { -x } n(5)")?, -5); - assert_eq!(engine.eval::("5 - -(-5)")?, 0); + assert_eq!(engine.eval::("fn neg(x) { -x } neg(5)")?, -5); + assert_eq!(engine.eval::("5 - -+++--+-5")?, 0); Ok(()) } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 5522ba67..f8371bb3 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -5,12 +5,15 @@ fn test_var_scope() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); let mut scope = Scope::new(); - engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; - assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 9); - engine.eval_with_scope::<()>(&mut scope, "x = x + 1; x = x + 2;")?; - assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 12); - assert_eq!(engine.eval_with_scope::<()>(&mut scope, "{let x = 3}")?, ()); - assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 12); + engine.eval_with_scope::<()>(&mut scope, false, "let x = 4 + 5")?; + assert_eq!(engine.eval_with_scope::(&mut scope, false, "x")?, 9); + engine.eval_with_scope::<()>(&mut scope, false, "x = x + 1; x = x + 2;")?; + assert_eq!(engine.eval_with_scope::(&mut scope, false, "x")?, 12); + assert_eq!( + engine.eval_with_scope::<()>(&mut scope, false, "{let x = 3}")?, + () + ); + assert_eq!(engine.eval_with_scope::(&mut scope, false, "x")?, 12); Ok(()) } @@ -25,24 +28,18 @@ fn test_scope_eval() -> Result<(), EvalAltResult> { // Then push some initialized variables into the state // NOTE: Remember the default numbers used by Rhai are i64 and f64. // Better stick to them or it gets hard to work with other variables in the script. - scope.push("y".into(), 42_i64); - scope.push("z".into(), 999_i64); + scope.push("y", 42_i64); + scope.push("z", 999_i64); // First invocation engine - .eval_with_scope::<()>( - &mut scope, - r" - let x = 4 + 5 - y + z; - y = 1; - ", - ) + .eval_with_scope::<()>(&mut scope, false, " let x = 4 + 5 - y + z; y = 1;") .expect("y and z not found?"); // Second invocation using the same state - if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); // should print 966 - } + let result = engine.eval_with_scope::(&mut scope, false, "x")?; + + println!("result: {}", result); // should print 966 // Variable y is changed in the script assert_eq!(scope.get_value::("y").unwrap(), 1);