Merge pull request #108 from schungx/master

Constants and aggressive optimizations.
This commit is contained in:
Stephen Chung 2020-03-16 13:12:57 +08:00 committed by GitHub
commit bffa3ed636
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1770 additions and 717 deletions

View File

@ -18,14 +18,16 @@ include = [
num-traits = "*"
[features]
#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked"]
default = []
#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"]
default = [ "optimize_full" ]
debug_msgs = [] # print debug messages on function registrations and calls
unchecked = [] # unchecked arithmetic
no_stdlib = [] # no standard library of utility functions
no_index = [] # no arrays and indexing
no_float = [] # no floating-point
no_function = [] # no script-defined functions
no_optimize = [] # no script optimizer
optimize_full = [] # set optimization level to Full (default is Simple)
only_i32 = [] # set INT=i32 (useful for 32-bit systems)
only_i64 = [] # set INT=i64 (default) and disable support for all other integer types

532
README.md
View File

@ -1,23 +1,26 @@
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.
Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application.
Rhai's current feature set:
* Easy integration with Rust functions and data types
* Fairly efficient (1 mil iterations in 0.75 sec on my 5 year old laptop)
* Easy integration with Rust functions and data types, supporting getter/setter methods
* Easily call a script-defined function from Rust
* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop)
* 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
* Very few additional dependencies (right now only [`num-traits`] to do checked arithmetic operations)
* Compiled script is optimized for repeat evaluations
* Very few additional dependencies (right now only [`num-traits`] to do checked arithmetic operations);
For [`no_std`] builds, a number of additional dependencies are pulled in to provide for basic library functionalities.
**Note:** Currently, the version is 0.10.2, so the language and API's may change before they stabilize.
Installation
------------
You can install Rhai using crates by adding this line to your dependencies:
Install the Rhai crate by adding this line to `dependencies`:
```toml
[dependencies]
@ -33,7 +36,7 @@ rhai = "*"
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`.
Beware that in order to use pre-releases (e.g. alpha and beta), the exact version must be specified in the `Cargo.toml`.
Optional features
-----------------
@ -43,13 +46,14 @@ Optional features
| `debug_msgs` | Print debug messages to stdout related to function registrations and calls. |
| `no_stdlib` | Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. Standard types are not affected. |
| `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! |
| `no_function` | Disable script-defined functions if you don't need them. |
| `no_index` | Disable arrays and indexing features if you don't need them. |
| `no_float` | Disable floating-point numbers and math if you don't need them. |
| `no_function` | Disable script-defined functions if not needed. |
| `no_index` | Disable arrays and indexing features if not needed. |
| `no_float` | Disable floating-point numbers and math if not needed. |
| `no_optimize` | Disable the script optimizer. |
| `only_i32` | Set the system integer type to `i32` and disable all other integer types. |
| `only_i64` | Set the system integer type to `i64` and disable all other integer types. |
By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here for you to opt-**out** of certain functionalities that you do not need.
By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed.
Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language.
Related
@ -57,8 +61,8 @@ Related
Other cool projects to check out:
* [ChaiScript] - 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] on [awesome-rust].
* [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.
* 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
--------
@ -66,13 +70,13 @@ Examples
A number of examples can be found in the `examples` folder:
| 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 |
| `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 |
| `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:
@ -107,8 +111,8 @@ There are also a number of examples scripts that showcase Rhai's features, all i
| `while.rhai` | while loop |
| Example scripts | Description |
| ----------------- | ----------------------------------------------------------------- |
| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter |
| ----------------- | ---------------------------------------------------------------------------------- |
| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter (1 million iterations) |
| `primes.rhai` | use Sieve of Eratosthenes to find all primes smaller than a limit |
To run the scripts, either make a tiny program or use of the `rhai_runner` example:
@ -137,13 +141,24 @@ fn main() -> Result<(), EvalAltResult>
}
```
You can also evaluate a script file:
The type parameter is used to specify the type of the return value, which _must_ match the actual type or an error is returned.
Rhai is very strict here. There are two ways to specify the return type - _turbofish_ notation, or type inference.
```rust
let result = engine.eval_file::<i64>("hello_world.rhai")?;
let result = engine.eval::<i64>("40 + 2")?; // return type is i64, specified using 'turbofish' notation
let result: i64 = engine.eval("40 + 2")?; // return type is inferred to be i64
let result = engine.eval<String>("40 + 2")?; // returns an error because the actual return type is i64, not String
```
If you want to repeatedly evaluate a script, you can _compile_ it first into an AST (abstract syntax tree) form:
Evaluate a script file directly:
```rust
let result = engine.eval_file::<i64>("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf'
```
To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form:
```rust
use rhai::Engine;
@ -154,7 +169,7 @@ let mut engine = Engine::new();
let ast = engine.compile("40 + 2")?;
for _ in 0..42 {
let result = engine.eval_ast::<i64>(&ast)?;
let result: i64 = engine.eval_ast(&ast)?;
println!("Answer: {}", result); // prints 42
}
@ -167,11 +182,10 @@ use rhai::Engine;
let mut engine = Engine::new();
let ast = engine.compile_file("hello_world.rhai".into()).unwrap();
let ast = engine.compile_file("hello_world.rhai".into())?;
```
Rhai also allows you to work _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust.
You do this via `call_fn`:
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust - via `call_fn`:
```rust
use rhai::Engine;
@ -179,8 +193,8 @@ use rhai::Engine;
let mut engine = Engine::new();
// Define a function in a script and load it into the Engine.
engine.consume(
r"
engine.consume(true, // pass true to 'retain_functions' otherwise these functions
r" // will be cleared at the end of consume()
fn hello(x, y) { // a function with two parameters: String and i64
x.len() + y // returning i64
}
@ -188,12 +202,11 @@ engine.consume(
fn hello(x) { // functions can be overloaded: this one takes only one parameter
x * 2 // returning i64
}
", true)?; // pass true to 'retain_functions' otherwise these functions
// will be cleared at the end of consume()
")?;
// Evaluate the function in the AST, passing arguments into the script as a tuple
// if there are more than one. Beware, arguments must be of the correct types because
// Rhai does not have built-in type conversions. If you pass in arguments of the wrong type,
// Rhai does not have built-in type conversions. If arguments of the wrong types are passed,
// the Engine will not find the function.
let result: i64 = engine.call_fn("hello", &ast, ( String::from("abc"), 123_i64 ) )?;
@ -218,19 +231,20 @@ The following primitive types are supported natively:
| **Dynamic** (i.e. can be anything) | `rhai::Dynamic` |
| **System** (current configuration) | `rhai::INT` (`i32` or `i64`),<br/>`rhai::FLOAT` (`f32` or `f64`) |
All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different; you cannot even add them together.
All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust.
The default integer type is `i64`. If you do not need any other integer type, you can enable the [`only_i64`] feature.
The default integer type is `i64`. If other integer types are not needed, it is possible to exclude them and make a smaller build with the [`only_i64`] feature.
If you only need 32-bit integers, you can enable the [`only_i32`] feature and remove support for all integer types other than `i32` including `i64`.
This is useful on 32-bit systems where using 64-bit integers incurs a performance penalty.
If only 32-bit integers are needed, enabling the [`only_i32`] feature will remove support for all integer types other than `i32`, including `i64`.
This is useful on 32-bit systems where using 64-bit integers incur a performance penalty.
If you do not need floating-point, enable the [`no_float`] feature to remove support.
If no floating-point is needed, use the [`no_float`] feature to remove support.
Value conversions
-----------------
There is a `to_float` function to convert a supported number to an `f64`, and a `to_int` function to convert a supported number to `i64` and that's about it. For other conversions you can register your own conversion functions.
There is a `to_float` function to convert a supported number to an `f64`, and a `to_int` function to convert a supported number to `i64` and that's about it.
For other conversions, register custom conversion functions.
There is also a `type_of` function to detect the type of a value.
@ -256,7 +270,8 @@ if z.type_of() == "string" {
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.
Rhai's scripting engine is very lightweight. It gets most of its abilities from functions.
To call these functions, they need to be registered with the engine.
```rust
use rhai::{Engine, EvalAltResult};
@ -294,7 +309,7 @@ fn main() -> Result<(), EvalAltResult>
}
```
To return a `Dynamic` value, simply `Box` it and return it.
To return a [`Dynamic`] value, simply `Box` it and return it.
```rust
fn decide(yes_no: bool) -> Dynamic {
@ -309,7 +324,7 @@ fn decide(yes_no: bool) -> Dynamic {
Generic functions
-----------------
Generic functions can be used in Rhai, but you'll need to register separate instances for each concrete type:
Generic functions can be used in Rhai, but separate instances for each concrete type must be registered separately:
```rust
use std::fmt::Display;
@ -330,14 +345,15 @@ 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 the correct one, based on the types of the parameters, from your script.
This example shows how to register multiple functions (or, in this case, multiple instances of the same function) to the same name in script.
This enables function overloading based on the number and types of parameters.
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).
If a function is _fallible_ (i.e. it returns a `Result<_, Error>`), it can be registered with `register_result_fn` (using the `RegisterResultFn` trait).
Your function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements `From<&str>` and `From<String>` etc. and the error text gets converted into `EvalAltResult::ErrorRuntime`.
The function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements `From<&str>` and `From<String>` etc. and the error text gets converted into `EvalAltResult::ErrorRuntime`.
```rust
use rhai::{Engine, EvalAltResult, Position};
@ -421,7 +437,7 @@ fn main() -> Result<(), EvalAltResult>
}
```
First, for each type we use with the engine, we need to be able to Clone. This allows the engine to pass by value and still keep its own state.
All custom types must implement `Clone`. This allows the [`Engine`] to pass by value.
```rust
#[derive(Clone)]
@ -430,7 +446,7 @@ struct TestStruct {
}
```
Next, we create a few methods that we'll later use in our scripts. Notice that we register our custom type with the engine.
Next, we create a few methods that we'll later use in our scripts. Notice that we register our custom type with the [`Engine`].
```rust
impl TestStruct {
@ -448,9 +464,9 @@ let mut engine = Engine::new();
engine.register_type::<TestStruct>();
```
To use methods and functions with the engine, we need to register them. There are some convenience functions to help with this. Below I register update and new with the engine.
To use methods and functions with the [`Engine`], we need to register them. There are some convenience functions to help with this. Below I register update and new with the [`Engine`].
*Note: the engine follows the convention that methods use a &mut first parameter so that invoking methods can update the value in memory.*
*Note: [`Engine`] follows the convention that methods use a `&mut` first parameter so that invoking methods can update the value in memory.*
```rust
engine.register_fn("update", TestStruct::update);
@ -486,12 +502,12 @@ let x = new_ts();
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.
If `register_type_with_name` is used to register the custom type with a special "pretty-print" name, `type_of` will return that name instead.
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.
Similarly, custom types can expose members by registering a `get` and/or `set` function.
For example:
@ -530,9 +546,11 @@ println!("result: {}", result);
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.
By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions that have been defined but no global state.
This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next,
such a state must be manually created and passed in.
In this example, we first create a state with a few initialized variables, then thread the same state through multiple invocations:
In this example, a global state object (a `Scope`) is created with a few initialized variables, then the same state is threaded through multiple invocations:
```rust
use rhai::{Engine, Scope, EvalAltResult};
@ -568,22 +586,25 @@ fn main() -> Result<(), EvalAltResult>
}
```
Rhai Language guide
Rhai Language Guide
===================
Comments
--------
Comments are C-style, including '`/*` ... `*/`' pairs and '`//`' for comments to the end of the line.
```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:
/* Fear not, Rhai satisfies all nesting needs with nested comments:
/*/*/*/*/**/*/*/*/*/
*/
```
@ -591,28 +612,85 @@ let /* intruder comment */ name = "Bob";
Variables
---------
Variables in Rhai follow normal naming rules (i.e. must contain only ASCII letters, digits and '`_`' underscores).
Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`').
Variable names must start with an ASCII letter or an underscore '`_`', and must contain at least one ASCII letter within.
Therefore, names like '`_`', '`_42`' etc. are not legal variable names. Variable names are also case _sensitive_.
Variables are defined using the `let` keyword.
```rust
let x = 3;
let x = 3; // ok
let _x = 42; // ok
let x_ = 42; // also ok
let _x_ = 42; // still ok
let _ = 123; // syntax error - illegal variable name
let _9 = 9; // syntax error - illegal variable name
let x = 42; // variable is 'x', lower case
let X = 123; // variable is 'X', upper case
x == 42;
X == 123;
```
Constants
---------
Constants can be defined using the `const` keyword and are immutable. Constants follow the same naming rules as [variables](#variables).
```rust
const x = 42;
print(x * 2); // prints 84
x = 123; // syntax error - cannot assign to constant
```
Constants must be assigned a _value_ not an expression.
```rust
const x = 40 + 2; // syntax error - cannot assign expression to constant
```
Numbers
-------
Integer numbers follow C-style format with support for decimal, binary ('`0b`'), octal ('`0o`') and hex ('`0x`') notations.
The default system integer type (also aliased to `INT`) is `i64`. It can be turned into `i32` via the [`only_i32`] feature.
Floating-point numbers are also supported if not disabled with [`no_float`]. The default system floating-point type is `i64` (also aliased to `FLOAT`).
'`_`' separators can be added freely and are ignored within a number.
| 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 |
| ---------------- | ---------------- |
| `123_345`, `-42` | `i64` in decimal |
| `0o07_76` | `i64` in octal |
| `0xabcd_ef` | `i64` in hex |
| `0b0101_1001` | `i64` in binary |
| `123_456.789` | `f64` |
Numeric operators
-----------------
Numeric operators generally follow C styles.
| Operator | Description | Integers only |
| -------- | ----------------------------------------------------------- | :-----------: |
| `+` | Plus | |
| `-` | Minus | |
| `*` | Multiply | |
| `/` | Divide (C-style integer division if acted on integer types) | |
| `%` | Modulo (remainder) | |
| `~` | Power | |
| `&` | Binary _And_ bit-mask | Yes |
| `|` | Binary _Or_ bit-mask | Yes |
| `^` | Binary _Xor_ bit-mask | Yes |
| `<<` | Left bit-shift | Yes |
| `>>` | Right bit-shift | Yes |
```rust
let x = (1 + 2) * (6 - 4) / 2; // arithmetic
let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses
let reminder = 42 % 10; // modulo
let power = 42 ~ 2; // power (i64 and f64 only)
let left_shifted = 42 << 3; // left shift
@ -623,10 +701,14 @@ let bit_op = 42 | 99; // bit masking
Unary operators
---------------
| Operator | Description |
| -------- | ----------- |
| `+` | Plus |
| `-` | Negative |
```rust
let number = -5;
number = -5 - +5;
let boolean = !true;
```
Numeric functions
@ -658,6 +740,17 @@ The following standard functions (defined in the standard library but excluded i
Strings and Chars
-----------------
String and char literals follow C-style formatting, with support for Unicode ('`\u`') and hex ('`\x`') escape sequences.
Although internally Rhai strings are stored as UTF-8 just like in Rust (they _are_ Rust `String`s),
in the Rhai language they can be considered a stream of Unicode characters, and can be directly indexed (unlike Rust).
Individual characters within a Rhai string can be replaced. In Rhai, there is no separate concepts of `String` and `&str` as in Rust.
Strings can be built up from other strings and types via the `+` operator (provided by the standard library but excluded if [`no_stdlib`]).
This is particularly useful when printing output.
`type_of()` a string returns `"string"`.
```rust
let name = "Bob";
let middle_initial = 'C';
@ -671,27 +764,28 @@ let age = 42;
let record = full_name + ": age " + age;
record == "Bob C. Davis: age 42";
// Strings can be indexed to get a character
// (disabled with the 'no_index' feature)
// Unlike Rust, Rhai strings can be indexed to get a character
// (disabled with 'no_index')
let c = record[4];
c == 'C';
ts.s = record;
ts.s = record; // custom type properties can take strings
let c = ts.s[4];
c == 'C';
let c = "foo"[0];
let c = "foo"[0]; // indexing also works on string literals...
c == 'f';
let c = ("foo" + "bar")[5];
let c = ("foo" + "bar")[5]; // ... and expressions returning strings
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
// Unlike Rust, Rhai strings can be directly modified character-by-character
// (disabled with 'no_index')
record[4] = '\x58'; // 0x58 = 'X'
record == "Bob X. Davis: age 42 ❤\n";
```
@ -741,7 +835,12 @@ full_name.len() == 0;
Arrays
------
You can create arrays of values, and then access them with numeric indices.
Arrays are first-class citizens in Rhai. Like C, arrays are accessed with zero-based, non-negative integer indices.
Array literals are built within square brackets '`[`' ,, '`]`' and separated by commas '`,`'.
The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`.
Arrays are disabled via the [`no_index`] feature.
The following functions (defined in the standard library but excluded if [`no_stdlib`]) operate on arrays:
@ -758,7 +857,7 @@ The following functions (defined in the standard library but excluded if [`no_st
Examples:
```rust
let y = [1, 2, 3]; // 3 elements
let y = [1, 2, 3]; // array literal with 3 elements
y[1] = 42;
print(y[1]); // prints 42
@ -770,7 +869,9 @@ foo == 42;
let foo = [1, 2, 3][0];
foo == 1;
fn abc() { [42, 43, 44] }
fn abc() {
[42, 43, 44] // a function returning an array literal
}
let foo = abc()[0];
foo == 42;
@ -804,8 +905,7 @@ y.clear(); // empty the array
print(y.len()); // prints 0
```
`push` and `pad` are only defined for standard built-in types. If you want to use them with
your own custom type, you need to register a type-specific version:
`push` and `pad` are only defined for standard built-in types. For custom types, type-specific versions must be registered:
```rust
engine.register_fn("push",
@ -813,27 +913,45 @@ engine.register_fn("push",
);
```
The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`.
Arrays are disabled via the [`no_index`] feature.
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`.
Comparing most values of the same data type work out-of-the-box for standard types supported by the system.
However, if the [`no_stdlib`] feature is turned on, comparisons can only be made between restricted system
types - `INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), string, array, `bool`, `char`.
```rust
42 == 42; // true
42 > 42; // false
"hello" > "foo"; // true
"42" == 42; // false
```
Comparing two values of _different_ data types, or of unknown data types, always results in `false`.
```rust
42 == 42.0; // false - i64 is different from f64
42 > "42"; // false - i64 is different from string
42 <= "42"; // false again
let ts = new_ts(); // custom type
ts == 42; // false - types are not the same
```
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.
| Operator | Description |
| -------- | ------------------------------- |
| `!` | Boolean _Not_ |
| `&&` | Boolean _And_ (short-circuits) |
| `||` | Boolean _Or_ (short-circuits) |
| `&` | Boolean _And_ (full evaluation) |
| `|` | Boolean _Or_ (full evaluation) |
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.
@ -869,34 +987,45 @@ my_str += 12345;
my_str == "abcABC12345"
```
If
--
If statements
-------------
All branches of an `if` statement must be enclosed within braces '`{`' .. '`}`', even when there is only one statement.
Like Rust, there is no ambiguity regarding which `if` clause a statement belongs to.
```rust
if true {
print("It's true!");
} else if true {
print("It's true again!");
} else if ... {
:
} else if ... {
:
} else {
print("It's false!");
print("It's finally false!");
}
if (decision) print("I've decided!");
// ^ syntax error, expecting '{' in statement block
```
While
-----
While loops
-----------
```rust
let x = 10;
while x > 0 {
print(x);
if x == 5 { break; }
if x == 5 { break; } // break out of while loop
x = x - 1;
}
```
Loop
----
Infinite loops
--------------
```rust
let x = 10;
@ -904,12 +1033,14 @@ let x = 10;
loop {
print(x);
x = x - 1;
if x == 0 { break; }
if x == 0 { break; } // break out of loop
}
```
For
---
For loops
---------
Iterating through a range or an array is provided by the `for` ... `in` loop.
```rust
let array = [1, 3, 5, 7, 9, 42];
@ -927,32 +1058,34 @@ for x in range(0, 50) {
}
```
Return
------
Returning values
----------------
```rust
return; // equivalent to return ();
return 123 + 456;
return 123 + 456; // returns 579
```
Errors and Exceptions
Errors and exceptions
---------------------
All of `Engine`'s evaluation/consuming methods return `Result<T, rhai::EvalAltResult>` with `EvalAltResult` holding error information.
To deliberately return an error during an evaluation, use the `throw` keyword.
```rust
if some_bad_condition_has_happened {
throw error; // 'throw' takes a string to form the exception text
}
throw; // no exception text
throw; // empty exception text: ""
```
All of `Engine`'s evaluation/consuming methods return `Result<T, rhai::EvalAltResult>` 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.
Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(`_reason_`, `_position_`))`
with the exception text captured by the first parameter.
```rust
let result = engine.eval::<i64>(&mut scope, r#"
let result = engine.eval::<i64>(r#"
let x = 42;
if x > 0 {
@ -976,34 +1109,44 @@ fn add(x, y) {
print(add(2, 3));
```
Just like in Rust, you can also use an implicit return.
Just like in Rust, an implicit return can be used. In fact, the last statement of a block is _always_ the block's return value
regardless of whether it is terminated with a semicolon `;`. This is different from Rust.
```rust
fn add(x, y) {
x + y
x + y; // value of the last statement is used as the function's return value
}
print(add(2, 3));
fn add2(x) {
return x + 2; // explicit return
}
print(add(2, 3)); // prints 5
print(add2(42)); // prints 44
```
Functions defined in script always take `Dynamic` parameters (i.e. the parameter can be of any type).
It is important to remember that all parameters are passed by _value_, so all functions are _pure_ (i.e. they never modify their parameters).
Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if you are not careful.
### Passing arguments by value
Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type).
It is important to remember that 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 not careful.
```rust
fn change(s) {
s = 42; // only a COPY of 'x' is changed
fn change(s) { // 's' is passed by value
s = 42; // only a COPY of 's' is changed
}
let x = 500;
x.change();
x.change(); // desugars to change(x)
x == 500; // 'x' is NOT changed!
```
Functions can only be defined at the top level, never inside a block or another function.
### Global definitions only
Functions can only be defined at the global level, never inside a block or another function.
```rust
// Top level is OK
// Global level is OK
fn add(x, y) {
x + y
}
@ -1018,7 +1161,9 @@ fn do_addition(x) {
}
```
Functions can be _overloaded_ based on the number of parameters (but not parameter types, since all parameters are `Dynamic`).
### Functions overloading
Functions can be _overloaded_ based on the _number_ of parameters (but not parameter _types_, since all parameters are the same type - [`Dynamic`]).
New definitions of the same name and number of parameters overwrite previous definitions.
```rust
@ -1037,15 +1182,19 @@ abc(); // prints "None."
Members and methods
-------------------
Properties and methods in a Rust custom type registered with the engine can be called just like in Rust:
```rust
let a = new_ts();
a.x = 500;
a.update();
let a = new_ts(); // constructor function
a.x = 500; // property access
a.update(); // method call
```
`print` and `debug`
-------------------
The `print` and `debug` functions default to printing to `stdout`, with `debug` using standard debug formatting.
```rust
print("hello"); // prints hello to stdout
print(1 + 2 + 3); // prints 6 to stdout
@ -1055,6 +1204,9 @@ debug("world!"); // prints "world!" to stdout using debug formatting
### Overriding `print` and `debug` with callback functions
When embedding Rhai into an application, it is usually necessary to trap `print` and `debug` output
(for logging into a tracking log, for example).
```rust
// Any function or closure that takes an &str argument can be used to override
// print and debug
@ -1077,10 +1229,11 @@ for entry in log {
}
```
Optimizations
=============
Script optimization
===================
Rhai includes an _optimizer_ that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed.
Script optimization can be turned off via the [`no_optimize`] feature.
For example, in the following:
@ -1108,47 +1261,132 @@ The above script optimizes to:
}
```
Constant propagation is used to remove dead code:
Constants propagation is used to remove dead code:
```rust
const ABC = true;
if ABC || some_work() { print("done!"); } // 'ABC' is constant so it is replaced by 'true'...
if true || some_work() { print("done!"); } // since '||' short-circuits, 'some_work' is never called
if true { print("done!"); } // <-- the line above is equivalent to this
print("done!"); // <-- the line above is further simplified to this
if true { print("done!"); } // <- the line above is equivalent to this
print("done!"); // <- the line above is further simplified to this
// because the condition is always true
```
These are quite effective for template-based machine-generated scripts where certain constant values are spliced into the script text in order to turn on/off certain sections.
These are quite effective for template-based machine-generated scripts where certain constant values
are spliced into the script text in order to turn on/off certain sections.
For fixed script texts, the constant values can be provided in a user-defined `Scope` object
to the `Engine` for use in compilation and evaluation.
Beware, however, that most operators are actually function calls, and those functions can be overridden, so they are not optimized away:
Beware, however, that most operators are actually function calls, and those functions can be overridden,
so they are not optimized away:
```rust
if 1 == 1 { ... } // '1==1' is NOT optimized away because you can define
// your own '==' function to override the built-in default!
const DECISION = 1;
if DECISION == 1 { // NOT optimized away because you can define
: // your own '==' function to override the built-in default!
:
} else if DECISION == 2 { // same here, NOT optimized away
:
} else if DECISION == 3 { // same here, NOT optimized away
:
} else {
:
}
```
### Here be dragons!
Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example:
because no operator functions will be run (in order not to trigger side effects) during the optimization process
(unless the optimization level is set to [`OptimizationLevel::Full`]). So, instead, do this:
```rust
if true { // <-- condition always true
123.456; // <-- eliminated
hello; // <-- eliminated, EVEN THOUGH the variable doesn't exist!
foo(42) // <-- promoted up-level
const DECISION_1 = true;
const DECISION_2 = false;
const DECISION_3 = false;
if DECISION_1 {
: // this branch is kept and promoted to the parent level
} else if DECISION_2 {
: // this branch is eliminated
} else if DECISION_3 {
: // this branch is eliminated
} else {
: // this branch is eliminated
}
```
In general, boolean constants are most effective for the optimizer to automatically prune
large `if`-`else` branches because they do not depend on operators.
Alternatively, turn the optimizer to [`OptimizationLevel::Full`]
Here be dragons!
----------------
### Optimization levels
There are actually three levels of optimizations: `None`, `Simple` and `Full`.
* `None` is obvious - no optimization on the AST is performed.
* `Simple` (default) performs relatively _safe_ optimizations without causing side effects
(i.e. it only relies on static analysis and will not actually perform any function calls).
* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
An engine's optimization level is set via a call to `set_optimization_level`:
```rust
// Turn on aggressive optimizations
engine.set_optimization_level(rhai::OptimizationLevel::Full);
```
```rust
// When compiling the following with OptimizationLevel::Full...
const DECISION = 1;
// this condition is now eliminated because 'DECISION == 1'
if DECISION == 1 { // is a function call to the '==' function, and it returns 'true'
print("hello!"); // this block is promoted to the parent level
} else {
print("boo!"); // this block is eliminated because it is never reached
}
// The above optimizes to:
foo(42)
print("hello!"); // <- the above is equivalent to this
```
Nevertheless, if you would be evaluating the original script, it would have been an error - the variable `hello` doesn't exist, so the script would have been terminated at that point with an error return.
### Side effect considerations
In fact, any errors inside a statement that has been eliminated will silently _go away_:
All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe.
Beware, however, that if custom functions are registered, they'll also be called.
If custom functions are registered to replace built-in operator functions, the custom functions will be called
and _may_ cause side-effects.
Therefore, when using [`OptimizationLevel::Full`], it is recommended that registrations of custom functions be held off
until _after_ the compilation process.
### Subtle semantic changes
Some optimizations can alter subtle semantics of the script. For example:
```rust
if true { // condition always true
123.456; // eliminated
hello; // eliminated, EVEN THOUGH the variable doesn't exist!
foo(42) // promoted up-level
}
foo(42) // <- the above optimizes to this
```
Nevertheless, if the original script were evaluated instead, it would have been an error - the variable `hello` doesn't exist,
so the script would have been terminated at that point with an error return.
In fact, any errors inside a statement that has been eliminated will silently _disappear_:
```rust
print("start!");
if my_decision { /* do nothing... */ } // <-- eliminated due to no effect
if my_decision { /* do nothing... */ } // eliminated due to no effect
print("end!");
// The above optimizes to:
@ -1158,21 +1396,22 @@ print("end!");
```
In the script above, if `my_decision` holds anything other than a boolean value, the script should have been terminated due to a type error.
However, after optimization, the entire `if` statement is removed, thus the script silently runs to completion without errors.
However, after optimization, the entire `if` statement is removed (because an access to `my_decision` produces no side effects),
thus the script silently runs to completion without errors.
### Turning off optimizations
It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess),
there is a setting in `Engine` to turn off optimizations.
turn it off by setting the optimization level to `OptimizationLevel::None`.
```rust
let engine = rhai::Engine::new();
engine.set_optimization(false); // turn off the optimizer
// Turn off the optimizer
engine.set_optimization_level(rhai::OptimizationLevel::None);
```
[ChaiScript]: http://chaiscript.com/
[scripting languages for Rust]: https://github.com/rust-unofficial/awesome-rust#scripting
[awesome-rust]: https://github.com/rust-unofficial/awesome-rust
[`num-traits`]: https://crates.io/crates/num-traits/
[`debug_msgs`]: #optional-features
[`unchecked`]: #optional-features
@ -1180,5 +1419,12 @@ engine.set_optimization(false); // turn off the optimizer
[`no_index`]: #optional-features
[`no_float`]: #optional-features
[`no_function`]: #optional-features
[`no_optimize`]: #optional-features
[`only_i32`]: #optional-features
[`only_i64`]: #optional-features
[`Engine`]: #hello-world
[`Scope`]: #initializing-and-maintaining-state
[`Dynamic`]: #values-and-types
[`OptimizationLevel::Full`]: #optimization-levels

View File

@ -1,4 +1,8 @@
use rhai::{Engine, EvalAltResult, Scope, AST};
#[cfg(not(feature = "no_optimize"))]
use rhai::OptimizationLevel;
use std::{
io::{stdin, stdout, Write},
iter,
@ -43,6 +47,10 @@ fn print_error(input: &str, err: EvalAltResult) {
fn main() {
let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
let mut scope = Scope::new();
let mut input = String::new();
@ -62,10 +70,11 @@ fn main() {
match input.as_str().trim() {
"exit" | "quit" => break, // quit
"ast" => {
if matches!(&ast, Some(_)) {
// print the last AST
match &ast {
Some(ast) => println!("{:#?}", ast),
None => println!("()"),
println!("{:#?}", ast.as_ref().unwrap());
} else {
println!("()");
}
continue;
}
@ -73,7 +82,7 @@ fn main() {
}
if let Err(err) = engine
.compile(&input)
.compile_with_scope(&scope, &input)
.map_err(EvalAltResult::ErrorParsing)
.and_then(|r| {
ast = Some(r);

View File

@ -1,4 +1,8 @@
use rhai::{Engine, EvalAltResult};
#[cfg(not(feature = "no_optimize"))]
use rhai::OptimizationLevel;
use std::{env, fs::File, io::Read, iter, process::exit};
fn padding(pad: &str, len: usize) -> String {
@ -49,6 +53,9 @@ fn main() {
for filename in env::args().skip(1) {
let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
let mut f = match File::open(&filename) {
Err(err) => {
eprintln!("Error reading script file: {}\n{}", filename, err);
@ -67,7 +74,7 @@ fn main() {
_ => (),
}
if let Err(err) = engine.consume(&contents, false) {
if let Err(err) = engine.consume(false, &contents) {
eprintln!("{}", padding("=", filename.len()));
eprintln!("{}", filename);
eprintln!("{}", padding("=", filename.len()));

View File

@ -1,12 +1,11 @@
// This is a script to calculate prime numbers.
let MAX_NUMBER_TO_CHECK = 10000; // 1229 primes
const MAX_NUMBER_TO_CHECK = 10000; // 1229 primes
let prime_mask = [];
prime_mask.pad(MAX_NUMBER_TO_CHECK, true);
prime_mask[0] = false;
prime_mask[1] = false;
prime_mask[0] = prime_mask[1] = false;
let total_primes_found = 0;

View File

@ -8,6 +8,10 @@ use crate::fn_register::RegisterFn;
use crate::parser::{lex, parse, FnDef, Position, AST};
use crate::result::EvalAltResult;
use crate::scope::Scope;
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize_ast;
use std::{
any::{type_name, TypeId},
fs::File,
@ -101,8 +105,14 @@ impl<'e> Engine<'e> {
/// Compile a string into an AST.
pub fn compile(&self, input: &str) -> Result<AST, ParseError> {
self.compile_with_scope(&Scope::new(), input)
}
/// Compile a string into an AST using own scope.
/// The scope is useful for passing constants into the script for optimization.
pub fn compile_with_scope(&self, scope: &Scope, input: &str) -> Result<AST, ParseError> {
let tokens_stream = lex(input);
parse(&mut tokens_stream.peekable(), self.optimize)
parse(&mut tokens_stream.peekable(), self, scope)
}
fn read_file(path: PathBuf) -> Result<String, EvalAltResult> {
@ -118,8 +128,20 @@ impl<'e> Engine<'e> {
/// Compile a file into an AST.
pub fn compile_file(&self, path: PathBuf) -> Result<AST, EvalAltResult> {
Self::read_file(path)
.and_then(|contents| self.compile(&contents).map_err(|err| err.into()))
self.compile_file_with_scope(&Scope::new(), path)
}
/// Compile a file into an AST using own scope.
/// The scope is useful for passing constants into the script for optimization.
pub fn compile_file_with_scope(
&self,
scope: &Scope,
path: PathBuf,
) -> Result<AST, EvalAltResult> {
Self::read_file(path).and_then(|contents| {
self.compile_with_scope(scope, &contents)
.map_err(|err| err.into())
})
}
/// Evaluate a file.
@ -127,6 +149,15 @@ impl<'e> Engine<'e> {
Self::read_file(path).and_then(|contents| self.eval::<T>(&contents))
}
/// Evaluate a file with own scope.
pub fn eval_file_with_scope<T: Any + Clone>(
&mut self,
scope: &mut Scope,
path: PathBuf,
) -> Result<T, EvalAltResult> {
Self::read_file(path).and_then(|contents| self.eval_with_scope::<T>(scope, &contents))
}
/// Evaluate a string.
pub fn eval<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> {
let mut scope = Scope::new();
@ -162,23 +193,21 @@ impl<'e> Engine<'e> {
) -> Result<Dynamic, EvalAltResult> {
engine.clear_functions();
#[cfg(feature = "no_function")]
let AST(statements) = ast;
#[cfg(not(feature = "no_function"))]
let statements = {
let AST(statements, functions) = ast;
engine.load_script_functions(functions);
statements
};
let result = statements
.iter()
.try_fold(().into_dynamic(), |_, stmt| engine.eval_stmt(scope, stmt));
let mut result = ().into_dynamic();
for stmt in statements {
result = engine.eval_stmt(scope, stmt)?;
}
engine.clear_functions();
result
Ok(result)
}
match eval_ast_internal(self, scope, ast) {
@ -207,10 +236,25 @@ impl<'e> Engine<'e> {
/// and not cleared from run to run.
pub fn consume_file(
&mut self,
path: PathBuf,
retain_functions: bool,
path: PathBuf,
) -> Result<(), EvalAltResult> {
Self::read_file(path).and_then(|contents| self.consume(&contents, retain_functions))
Self::read_file(path).and_then(|contents| self.consume(retain_functions, &contents))
}
/// Evaluate a file with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume_file_with_scope(
&mut self,
scope: &mut Scope,
retain_functions: bool,
path: PathBuf,
) -> Result<(), EvalAltResult> {
Self::read_file(path)
.and_then(|contents| self.consume_with_scope(scope, retain_functions, &contents))
}
/// Evaluate a string, but throw away the result and only return error (if any).
@ -218,11 +262,11 @@ impl<'e> Engine<'e> {
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume(&mut self, input: &str, retain_functions: bool) -> Result<(), EvalAltResult> {
pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> {
self.consume_with_scope(&mut Scope::new(), retain_functions, input)
}
/// Evaluate a string, but throw away the result and only return error (if any).
/// Evaluate a string with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
@ -235,7 +279,7 @@ impl<'e> Engine<'e> {
) -> Result<(), EvalAltResult> {
let tokens_stream = lex(input);
let ast = parse(&mut tokens_stream.peekable(), self.optimize)
let ast = parse(&mut tokens_stream.peekable(), self, scope)
.map_err(EvalAltResult::ErrorParsing)?;
self.consume_ast_with_scope(scope, retain_functions, &ast)
@ -246,6 +290,15 @@ impl<'e> Engine<'e> {
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> {
self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast)
}
/// Evaluate an AST with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume_ast_with_scope(
&mut self,
scope: &mut Scope,
@ -256,10 +309,6 @@ impl<'e> Engine<'e> {
self.clear_functions();
}
#[cfg(feature = "no_function")]
let AST(statements) = ast;
#[cfg(not(feature = "no_function"))]
let statements = {
let AST(ref statements, ref functions) = ast;
self.load_script_functions(functions);
@ -307,7 +356,7 @@ impl<'e> Engine<'e> {
///
/// let mut engine = Engine::new();
///
/// engine.consume("fn add(x, y) { x.len() + y }", true)?;
/// engine.consume(true, "fn add(x, y) { x.len() + y }")?;
///
/// let result: i64 = engine.call_fn("add", (String::from("abc"), 123_i64))?;
///
@ -345,6 +394,27 @@ impl<'e> Engine<'e> {
})
}
/// Optimize the AST with constants defined in an external Scope.
/// An optimized copy of the AST is returned while the original AST is untouched.
///
/// Although optimization is performed by default during compilation, sometimes it is necessary to
/// _re_-optimize an AST. For example, when working with constants that are passed in via an
/// external scope, it will be more efficient to optimize the AST once again to take advantage
/// of the new constants.
///
/// With this method, it is no longer necessary to recompile a large script. The script AST can be
/// compiled just once. Before evaluation, constants are passed into the `Engine` via an external scope
/// (i.e. with `scope.push_constant(...)`). Then, the AST is cloned and the copy re-optimized before running.
#[cfg(not(feature = "no_optimize"))]
pub fn optimize_ast(&self, scope: &Scope, ast: &AST) -> AST {
optimize_ast(
self,
scope,
ast.0.clone(),
ast.1.iter().map(|f| (**f).clone()).collect(),
)
}
/// Override default action of `print` (print to stdout using `println!`)
///
/// # Example
@ -359,7 +429,7 @@ impl<'e> Engine<'e> {
///
/// // Override action of 'print' function
/// engine.on_print(|s| result.push_str(s));
/// engine.consume("print(40 + 2);", false)?;
/// engine.consume(false, "print(40 + 2);")?;
/// }
/// assert_eq!(result, "42");
/// # Ok(())
@ -383,7 +453,7 @@ impl<'e> Engine<'e> {
///
/// // Override action of 'debug' function
/// engine.on_debug(|s| result.push_str(s));
/// engine.consume(r#"debug("hello");"#, false)?;
/// engine.consume(false, r#"debug("hello");"#)?;
/// }
/// assert_eq!(result, "\"hello\"");
/// # Ok(())

View File

@ -8,7 +8,9 @@ use crate::engine::Engine;
use crate::fn_register::{RegisterFn, RegisterResultFn};
use crate::parser::{Position, INT};
use crate::result::EvalAltResult;
use crate::FLOAT;
#[cfg(not(feature = "no_float"))]
use crate::parser::FLOAT;
use num_traits::{
identities::Zero, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedShl,
@ -578,7 +580,7 @@ impl Engine<'_> {
}
fn range<T>(from: T, to: T) -> Range<T> {
(from..to)
from..to
}
reg_iterator::<INT>(self);
@ -771,9 +773,12 @@ impl Engine<'_> {
self.register_dynamic_fn("pop", |list: &mut Array| {
list.pop().unwrap_or_else(|| ().into_dynamic())
});
self.register_dynamic_fn("shift", |list: &mut Array| match list.len() {
0 => ().into_dynamic(),
_ => list.remove(0),
self.register_dynamic_fn("shift", |list: &mut Array| {
if !list.is_empty() {
().into_dynamic()
} else {
list.remove(0)
}
});
self.register_fn("len", |list: &mut Array| list.len() as INT);
self.register_fn("clear", |list: &mut Array| list.clear());

View File

@ -1,6 +1,8 @@
//! Helper module which defines `FnArgs` to make function calling easier.
use crate::any::{Any, Dynamic};
#[cfg(not(feature = "no_index"))]
use crate::engine::Array;
/// Trait that represent arguments to a function call.

View File

@ -3,7 +3,10 @@
use crate::any::{Any, AnyExt, Dynamic, Variant};
use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
use crate::result::EvalAltResult;
use crate::scope::Scope;
use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::OptimizationLevel;
#[cfg(not(feature = "no_index"))]
use crate::INT;
@ -63,17 +66,20 @@ pub struct FnSpec<'a> {
/// ```
pub struct Engine<'e> {
/// Optimize the AST after compilation
pub(crate) optimize: bool,
#[cfg(not(feature = "no_optimize"))]
pub(crate) optimization_level: OptimizationLevel,
/// A hashmap containing all compiled functions known to the engine
pub(crate) ext_functions: HashMap<FnSpec<'e>, Box<FnAny>>,
/// A hashmap containing all script-defined functions
pub(crate) script_functions: Vec<Arc<FnDef>>,
/// A hashmap containing all iterators known to the engine
pub(crate) type_iterators: HashMap<TypeId, Box<IteratorFn>>,
/// A hashmap mapping type names to pretty-print names
pub(crate) type_names: HashMap<String, String>,
// Closures for implementing the print/debug commands
/// Closure for implementing the print commands
pub(crate) on_print: Box<dyn FnMut(&str) + 'e>,
/// Closure for implementing the debug commands
pub(crate) on_debug: Box<dyn FnMut(&str) + 'e>,
}
@ -93,13 +99,20 @@ impl Engine<'_> {
// Create the new scripting Engine
let mut engine = Engine {
optimize: true,
ext_functions: HashMap::new(),
script_functions: Vec::new(),
type_iterators: HashMap::new(),
type_names,
on_print: Box::new(default_print), // default print/debug implementations
on_debug: Box::new(default_print),
#[cfg(not(feature = "no_optimize"))]
#[cfg(not(feature = "optimize_full"))]
optimization_level: OptimizationLevel::Simple,
#[cfg(not(feature = "no_optimize"))]
#[cfg(feature = "optimize_full")]
optimization_level: OptimizationLevel::Full,
};
engine.register_core_lib();
@ -110,9 +123,32 @@ impl Engine<'_> {
engine
}
/// Control whether the `Engine` will optimize an AST after compilation
pub fn set_optimization(&mut self, optimize: bool) {
self.optimize = optimize
/// Control whether and how the `Engine` will optimize an AST after compilation
#[cfg(not(feature = "no_optimize"))]
pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) {
self.optimization_level = optimization_level
}
/// Call a registered function
#[cfg(not(feature = "no_optimize"))]
pub(crate) fn call_ext_fn_raw(
&self,
fn_name: &str,
args: FnCallArgs,
pos: Position,
) -> Result<Option<Dynamic>, EvalAltResult> {
let spec = FnSpec {
name: fn_name.into(),
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
};
// Search built-in's and external functions
if let Some(func) = self.ext_functions.get(&spec) {
// Run external function
Ok(Some(func(args, pos)?))
} else {
Ok(None)
}
}
/// Universal method for calling functions, that are either
@ -148,12 +184,13 @@ impl Engine<'_> {
fn_def
.params
.iter()
.zip(args.iter().map(|x| (*x).into_dynamic())),
.zip(args.iter().map(|x| (*x).into_dynamic()))
.map(|(name, value)| (name, VariableType::Normal, value)),
);
// Evaluate
return match self.eval_stmt(&mut scope, &fn_def.body) {
// Convert return statement to return value
return match self.eval_stmt(&mut scope, &fn_def.body) {
Err(EvalAltResult::Return(x, _)) => Ok(x),
other => other,
};
@ -164,13 +201,13 @@ impl Engine<'_> {
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
};
// Then search built-in's and external functions
// Search built-in's and external functions
if let Some(func) = self.ext_functions.get(&spec) {
// Run external function
let result = func(args, pos)?;
// See if the function match print/debug (which requires special processing)
let callback = match spec.name.as_ref() {
let callback = match fn_name {
KEYWORD_PRINT => self.on_print.as_mut(),
KEYWORD_DEBUG => self.on_debug.as_mut(),
_ => return Ok(result),
@ -184,7 +221,7 @@ impl Engine<'_> {
return Ok(callback(val).into_dynamic());
}
if spec.name == KEYWORD_TYPE_OF && args.len() == 1 {
if fn_name == KEYWORD_TYPE_OF && args.len() == 1 {
// Handle `type_of` function
return Ok(self
.map_type_name(args[0].type_name())
@ -192,23 +229,23 @@ impl Engine<'_> {
.into_dynamic());
}
if spec.name.starts_with(FUNC_GETTER) {
if fn_name.starts_with(FUNC_GETTER) {
// Getter function not found
return Err(EvalAltResult::ErrorDotExpr(
format!(
"- property '{}' unknown or write-only",
&spec.name[FUNC_GETTER.len()..]
&fn_name[FUNC_GETTER.len()..]
),
pos,
));
}
if spec.name.starts_with(FUNC_SETTER) {
if fn_name.starts_with(FUNC_SETTER) {
// Setter function not found
return Err(EvalAltResult::ErrorDotExpr(
format!(
"- property '{}' unknown or read-only",
&spec.name[FUNC_SETTER.len()..]
&fn_name[FUNC_SETTER.len()..]
),
pos,
));
@ -227,7 +264,7 @@ impl Engine<'_> {
.collect::<Vec<_>>();
Err(EvalAltResult::ErrorFunctionNotFound(
format!("{} ({})", spec.name, types_list.join(", ")),
format!("{} ({})", fn_name, types_list.join(", ")),
pos,
))
}
@ -248,14 +285,14 @@ impl Engine<'_> {
.collect::<Result<Vec<_>, _>>()?;
let args = once(this_ptr)
.chain(values.iter_mut().map(|b| b.as_mut()))
.chain(values.iter_mut().map(Dynamic::as_mut))
.collect();
self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos)
}
// xxx.id
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -266,7 +303,7 @@ impl Engine<'_> {
Expr::Index(idx_lhs, idx_expr, idx_pos) => {
let (val, _) = match idx_lhs.as_ref() {
// xxx.id[idx_expr]
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
(
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
@ -294,7 +331,7 @@ impl Engine<'_> {
// xxx.dot_lhs.rhs
Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() {
// xxx.id.rhs
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -305,7 +342,7 @@ impl Engine<'_> {
Expr::Index(idx_lhs, idx_expr, idx_pos) => {
let (val, _) = match idx_lhs.as_ref() {
// xxx.id[idx_expr].rhs
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
(
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
@ -353,8 +390,8 @@ impl Engine<'_> {
) -> Result<Dynamic, EvalAltResult> {
match dot_lhs {
// id.???
Expr::Identifier(id, pos) => {
let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
Expr::Variable(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.
@ -371,7 +408,15 @@ impl Engine<'_> {
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 {
if let Some((id, var_type, src_idx)) = src {
match var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
idx_lhs.position(),
));
}
VariableType::Normal => {
Self::update_indexed_var_in_scope(
src_type,
scope,
@ -379,9 +424,11 @@ impl Engine<'_> {
src_idx,
idx,
target,
idx_lhs.position(),
dot_rhs.position(),
)?;
}
}
}
val
}
@ -400,11 +447,11 @@ impl Engine<'_> {
id: &str,
map: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>,
begin: Position,
) -> Result<(usize, T), EvalAltResult> {
) -> Result<(usize, VariableType, T), EvalAltResult> {
scope
.get(id)
.ok_or_else(|| EvalAltResult::ErrorVariableNotFound(id.into(), begin))
.and_then(move |(idx, _, val)| map(val).map(|v| (idx, v)))
.and_then(move |(idx, _, var_type, val)| map(val).map(|v| (idx, var_type, v)))
}
/// Evaluate the value of an index (must evaluate to INT)
@ -476,19 +523,32 @@ impl Engine<'_> {
lhs: &'a Expr,
idx_expr: &Expr,
idx_pos: Position,
) -> Result<(IndexSourceType, Option<(&'a str, usize)>, usize, Dynamic), EvalAltResult> {
) -> Result<
(
IndexSourceType,
Option<(&'a str, VariableType, usize)>,
usize,
Dynamic,
),
EvalAltResult,
> {
let idx = self.eval_index_value(scope, idx_expr)?;
match lhs {
// id[idx_expr]
Expr::Identifier(id, _) => Self::search_scope(
Expr::Variable(id, _) => Self::search_scope(
scope,
&id,
|val| self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos),
lhs.position(),
)
.map(|(src_idx, (val, src_type))| {
(src_type, Some((id.as_str(), src_idx)), idx as usize, val)
.map(|(src_idx, var_type, (val, src_type))| {
(
src_type,
Some((id.as_str(), var_type, src_idx)),
idx as usize,
val,
)
}),
// (expr)[idx_expr]
@ -543,8 +603,7 @@ impl Engine<'_> {
Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic())
}
// All other variable types should be an error
_ => panic!("array or string source type expected for indexing"),
IndexSourceType::Expression => panic!("expression cannot be indexed for update"),
}
}
@ -585,7 +644,7 @@ impl Engine<'_> {
) -> Result<Dynamic, EvalAltResult> {
match dot_rhs {
// xxx.id
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let set_fn_name = format!("{}{}", FUNC_SETTER, id);
self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.as_mut()], None, *pos)
@ -596,7 +655,7 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
// xxx.id[idx_expr]
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -620,7 +679,7 @@ impl Engine<'_> {
// xxx.lhs.{...}
Expr::Dot(lhs, rhs, _) => match lhs.as_ref() {
// xxx.id.rhs
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -640,7 +699,7 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
// xxx.id[idx_expr].rhs
Expr::Identifier(id, pos) => {
Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -702,11 +761,23 @@ impl Engine<'_> {
dot_rhs: &Expr,
new_val: Dynamic,
val_pos: Position,
op_pos: Position,
) -> Result<Dynamic, EvalAltResult> {
match dot_lhs {
// id.???
Expr::Identifier(id, pos) => {
let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
Expr::Variable(id, pos) => {
let (src_idx, var_type, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
match var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
op_pos,
))
}
_ => (),
}
let val =
self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos);
@ -726,17 +797,21 @@ impl Engine<'_> {
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_var_in_scope(
src_type,
scope,
id,
src_idx,
idx,
target,
if let Some((id, var_type, src_idx)) = src {
match var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
lhs.position(),
));
}
VariableType::Normal => {
Self::update_indexed_var_in_scope(
src_type, scope, id, src_idx, idx, target, val_pos,
)?;
}
}
}
val
}
@ -758,9 +833,10 @@ impl Engine<'_> {
Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()),
Expr::StringConstant(s, _) => Ok(s.into_dynamic()),
Expr::CharConstant(c, _) => Ok(c.into_dynamic()),
Expr::Identifier(id, pos) => {
Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val)
Expr::Variable(id, pos) => {
Self::search_scope(scope, id, Ok, *pos).map(|(_, _, val)| val)
}
Expr::Property(_, _) => panic!("unexpected property."),
// lhs[idx_expr]
#[cfg(not(feature = "no_index"))]
@ -768,26 +844,25 @@ impl Engine<'_> {
.eval_index_expr(scope, lhs, idx_expr, *idx_pos)
.map(|(_, _, _, x)| x),
#[cfg(feature = "no_index")]
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
// Statement block
Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt),
// lhs = rhs
Expr::Assignment(lhs, rhs, _) => {
Expr::Assignment(lhs, rhs, op_pos) => {
let rhs_val = self.eval_expr(scope, rhs)?;
match lhs.as_ref() {
// name = rhs
Expr::Identifier(name, pos) => {
if let Some((idx, _, _)) = scope.get(name) {
*scope.get_mut(name, idx) = rhs_val;
Ok(().into_dynamic())
} else {
Err(EvalAltResult::ErrorVariableNotFound(name.clone(), *pos))
}
Expr::Variable(name, pos) => match scope.get(name) {
Some((idx, _, VariableType::Normal, _)) => {
*scope.get_mut(name, idx) = rhs_val.clone();
Ok(rhs_val)
}
Some((_, _, VariableType::Constant, _)) => Err(
EvalAltResult::ErrorAssignmentToConstant(name.to_string(), *op_pos),
),
_ => Err(EvalAltResult::ErrorVariableNotFound(name.clone(), *pos)),
},
// idx_lhs[idx_expr] = rhs
#[cfg(not(feature = "no_index"))]
@ -795,8 +870,15 @@ impl Engine<'_> {
let (src_type, src, idx, _) =
self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?;
if let Some((id, src_idx)) = src {
Ok(Self::update_indexed_var_in_scope(
if let Some((id, var_type, src_idx)) = src {
match var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
idx_lhs.position(),
));
}
VariableType::Normal => Ok(Self::update_indexed_var_in_scope(
src_type,
scope,
&id,
@ -804,7 +886,8 @@ impl Engine<'_> {
idx,
rhs_val,
rhs.position(),
)?)
)?),
}
} else {
Err(EvalAltResult::ErrorAssignmentToUnknownLHS(
idx_lhs.position(),
@ -814,9 +897,15 @@ impl Engine<'_> {
// dot_lhs.dot_rhs = rhs
Expr::Dot(dot_lhs, dot_rhs, _) => {
self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position())
self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position(), *op_pos)
}
// Error assignment to constant
expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant(
expr.get_constant_str(),
lhs.position(),
)),
// Syntax error
_ => Err(EvalAltResult::ErrorAssignmentToUnknownLHS(lhs.position())),
}
@ -834,8 +923,6 @@ impl Engine<'_> {
Ok(Box::new(arr))
}
#[cfg(feature = "no_index")]
Expr::Array(_, _) => panic!("encountered an array during no_index!"),
// Dump AST
Expr::FunctionCall(fn_name, args_expr_list, _, pos) if fn_name == KEYWORD_DUMP_AST => {
@ -925,7 +1012,16 @@ impl Engine<'_> {
Stmt::Noop(_) => Ok(().into_dynamic()),
// Expression as statement
Stmt::Expr(expr) => self.eval_expr(scope, expr),
Stmt::Expr(expr) => {
let result = self.eval_expr(scope, expr)?;
Ok(if !matches!(expr.as_ref(), Expr::Assignment(_, _, _)) {
result
} else {
// If it is an assignment, erase the result at the root
().into_dynamic()
})
}
// Block scope
Stmt::Block(block, _) => {
@ -967,9 +1063,9 @@ impl Engine<'_> {
Ok(guard_val) => {
if *guard_val {
match self.eval_stmt(scope, body) {
Ok(_) => (),
Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()),
Err(x) => return Err(x),
_ => (),
}
} else {
return Ok(().into_dynamic());
@ -982,9 +1078,9 @@ impl Engine<'_> {
// Loop statement
Stmt::Loop(body) => loop {
match self.eval_stmt(scope, body) {
Ok(_) => (),
Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()),
Err(x) => return Err(x),
_ => (),
}
},
@ -1001,9 +1097,9 @@ impl Engine<'_> {
*scope.get_mut(name, idx) = a;
match self.eval_stmt(scope, body) {
Ok(_) => (),
Err(EvalAltResult::LoopBreak) => break,
Err(x) => return Err(x),
_ => (),
}
}
scope.pop();
@ -1045,7 +1141,7 @@ impl Engine<'_> {
// Let statement
Stmt::Let(name, Some(expr), _) => {
let val = self.eval_expr(scope, expr)?;
scope.push_dynamic(name.clone(), val);
scope.push_dynamic(name.clone(), VariableType::Normal, val);
Ok(().into_dynamic())
}
@ -1053,6 +1149,15 @@ impl Engine<'_> {
scope.push(name.clone(), ());
Ok(().into_dynamic())
}
// Const statement
Stmt::Const(name, expr, _) if expr.is_constant() => {
let val = self.eval_expr(scope, expr)?;
scope.push_dynamic(name.clone(), VariableType::Constant, val);
Ok(().into_dynamic())
}
Stmt::Const(_, _, _) => panic!("constant expression not constant!"),
}
}

View File

@ -18,20 +18,11 @@ pub enum LexError {
MalformedChar(String),
/// Error in the script text.
InputError(String),
/// An identifier is in an invalid format.
MalformedIdentifier(String),
}
impl Error for LexError {
fn description(&self) -> &str {
match *self {
Self::UnexpectedChar(_) => "Unexpected character",
Self::UnterminatedString => "Open string is not terminated",
Self::MalformedEscapeSequence(_) => "Unexpected values in escape sequence",
Self::MalformedNumber(_) => "Unexpected characters in number",
Self::MalformedChar(_) => "Char constant not a single character",
Self::InputError(_) => "Input error",
}
}
}
impl Error for LexError {}
impl fmt::Display for LexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -40,8 +31,11 @@ impl fmt::Display for LexError {
Self::MalformedEscapeSequence(s) => write!(f, "Invalid escape sequence: '{}'", s),
Self::MalformedNumber(s) => write!(f, "Invalid number: '{}'", s),
Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s),
Self::MalformedIdentifier(s) => {
write!(f, "Variable name is not in a legal format: '{}'", s)
}
Self::InputError(s) => write!(f, "{}", s),
_ => write!(f, "{}", self.description()),
Self::UnterminatedString => write!(f, "Open string is not terminated"),
}
}
}
@ -62,21 +56,34 @@ pub enum ParseErrorType {
/// An open `{` is missing the corresponding closing `}`.
MissingRightBrace(String),
/// An open `[` is missing the corresponding closing `]`.
#[cfg(not(feature = "no_index"))]
MissingRightBracket(String),
/// An expression in function call arguments `()` has syntax error.
MalformedCallExpr(String),
/// An expression in indexing brackets `[]` has syntax error.
#[cfg(not(feature = "no_index"))]
MalformedIndexExpr(String),
/// Missing a variable name after the `let` keyword.
VarExpectsIdentifier,
/// Invalid expression assigned to constant.
ForbiddenConstantExpr(String),
/// Missing a variable name after the `let`, `const` or `for` keywords.
VariableExpected,
/// A `for` statement is missing the `in` keyword.
MissingIn,
/// Defining a function `fn` in an appropriate place (e.g. inside another function).
#[cfg(not(feature = "no_function"))]
WrongFnDefinition,
/// Missing a function name after the `fn` keyword.
#[cfg(not(feature = "no_function"))]
FnMissingName,
/// A function definition is missing the parameters list. Wrapped value is the function name.
#[cfg(not(feature = "no_function"))]
FnMissingParams(String),
/// Assignment to an inappropriate LHS (left-hand-side) expression.
AssignmentToInvalidLHS,
/// Assignment to a copy of a value.
AssignmentToCopy,
/// Assignment to an a constant variable.
AssignmentToConstant(String),
}
/// Error when parsing a script.
@ -98,10 +105,8 @@ impl ParseError {
pub fn position(&self) -> Position {
self.1
}
}
impl Error for ParseError {
fn description(&self) -> &str {
pub(crate) fn desc(&self) -> &str {
match self.0 {
ParseErrorType::BadInput(ref p) => p,
ParseErrorType::InputPastEndOfFile => "Script is incomplete",
@ -109,40 +114,64 @@ impl Error for ParseError {
ParseErrorType::MissingRightParen(_) => "Expecting ')'",
ParseErrorType::MissingLeftBrace => "Expecting '{'",
ParseErrorType::MissingRightBrace(_) => "Expecting '}'",
#[cfg(not(feature = "no_index"))]
ParseErrorType::MissingRightBracket(_) => "Expecting ']'",
ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments",
#[cfg(not(feature = "no_index"))]
ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression",
ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable",
ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant",
ParseErrorType::MissingIn => "Expecting 'in'",
ParseErrorType::VariableExpected => "Expecting name of a variable",
#[cfg(not(feature = "no_function"))]
ParseErrorType::FnMissingName => "Expecting name in function declaration",
#[cfg(not(feature = "no_function"))]
ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration",
#[cfg(not(feature = "no_function"))]
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"
ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression",
ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value",
ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable."
}
}
}
fn cause(&self) -> Option<&dyn Error> {
None
}
}
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)
| ParseErrorType::MalformedIndexExpr(ref s)
| ParseErrorType::MalformedCallExpr(ref s) => {
write!(f, "{}", if s.is_empty() { self.description() } else { s })?
ParseErrorType::BadInput(ref s) | ParseErrorType::MalformedCallExpr(ref s) => {
write!(f, "{}", if s.is_empty() { self.desc() } else { s })?
}
ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.description(), s)?,
ParseErrorType::ForbiddenConstantExpr(ref s) => {
write!(f, "Expecting a constant to assign to '{}'", s)?
}
ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.desc(), s)?,
#[cfg(not(feature = "no_index"))]
ParseErrorType::MalformedIndexExpr(ref s) => {
write!(f, "{}", if s.is_empty() { self.desc() } else { s })?
}
#[cfg(not(feature = "no_function"))]
ParseErrorType::FnMissingParams(ref s) => {
write!(f, "Expecting parameters for function '{}'", s)?
}
ParseErrorType::MissingRightParen(ref s)
| ParseErrorType::MissingRightBrace(ref s)
| ParseErrorType::MissingRightBracket(ref s) => {
write!(f, "{} for {}", self.description(), s)?
ParseErrorType::MissingRightParen(ref s) | ParseErrorType::MissingRightBrace(ref s) => {
write!(f, "{} for {}", self.desc(), s)?
}
_ => write!(f, "{}", self.description())?,
#[cfg(not(feature = "no_index"))]
ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?,
ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => {
write!(f, "{}", self.desc())?
}
ParseErrorType::AssignmentToConstant(ref s) => {
write!(f, "Cannot assign to constant '{}'", s)?
}
_ => write!(f, "{}", self.desc())?,
}
if !self.1.is_eof() {

View File

@ -196,13 +196,10 @@ macro_rules! def_register {
// 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) => {
f($(($clone)($par)),*).map(|r| Box::new(r) as Dynamic).map_err(|mut err| {
err.set_position(pos);
Err(err)
}
}
err
})
};
self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun));
}

View File

@ -78,9 +78,15 @@ pub use call::FuncArgs;
pub use engine::Engine;
pub use error::{ParseError, ParseErrorType};
pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn};
pub use parser::{Position, AST, FLOAT, INT};
pub use parser::{Position, AST, INT};
pub use result::EvalAltResult;
pub use scope::Scope;
pub use scope::{Scope, ScopeEntry, VariableType};
#[cfg(not(feature = "no_index"))]
pub use engine::Array;
#[cfg(not(feature = "no_float"))]
pub use parser::FLOAT;
#[cfg(not(feature = "no_optimize"))]
pub use optimize::OptimizationLevel;

View File

@ -1,17 +1,78 @@
use crate::engine::KEYWORD_DUMP_AST;
use crate::parser::{Expr, Stmt};
#![cfg(not(feature = "no_optimize"))]
fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt {
use crate::any::Dynamic;
use crate::engine::{Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT};
use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST};
use crate::scope::{Scope, ScopeEntry, VariableType};
use std::sync::Arc;
/// Level of optimization performed
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
pub enum OptimizationLevel {
/// No optimization performed
None,
/// Only perform simple optimizations without evaluating functions
Simple,
/// Full optimizations performed, including evaluating functions.
/// Take care that this may cause side effects.
Full,
}
struct State<'a> {
changed: bool,
constants: Vec<(String, Expr)>,
engine: Option<&'a Engine<'a>>,
}
impl State<'_> {
pub fn new() -> Self {
State {
changed: false,
constants: vec![],
engine: None,
}
}
pub fn reset(&mut self) {
self.changed = false;
}
pub fn set_dirty(&mut self) {
self.changed = true;
}
pub fn is_dirty(&self) -> bool {
self.changed
}
pub fn contains_constant(&self, name: &str) -> bool {
self.constants.iter().any(|(n, _)| n == name)
}
pub fn restore_constants(&mut self, len: usize) {
self.constants.truncate(len)
}
pub fn push_constant(&mut self, name: &str, value: Expr) {
self.constants.push((name.to_string(), value))
}
pub fn find_constant(&self, name: &str) -> Option<&Expr> {
for (n, expr) in self.constants.iter().rev() {
if n == name {
return Some(expr);
}
}
None
}
}
fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt {
match stmt {
Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => {
*changed = true;
state.set_dirty();
let pos = expr.position();
let expr = optimize_expr(*expr, changed);
let expr = optimize_expr(*expr, state);
match expr {
Expr::False(_) | Expr::True(_) => Stmt::Noop(stmt1.position()),
expr => {
if matches!(expr, Expr::False(_) | Expr::True(_)) {
Stmt::Noop(stmt1.position())
} else {
let stmt = Stmt::Expr(Box::new(expr));
if preserve_result {
@ -21,28 +82,27 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
}
}
}
}
Stmt::IfElse(expr, stmt1, None) => match *expr {
Expr::False(pos) => {
*changed = true;
state.set_dirty();
Stmt::Noop(pos)
}
Expr::True(_) => optimize_stmt(*stmt1, changed, true),
Expr::True(_) => optimize_stmt(*stmt1, state, true),
expr => Stmt::IfElse(
Box::new(optimize_expr(expr, changed)),
Box::new(optimize_stmt(*stmt1, changed, true)),
Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*stmt1, state, true)),
None,
),
},
Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr {
Expr::False(_) => optimize_stmt(*stmt2, changed, true),
Expr::True(_) => optimize_stmt(*stmt1, changed, true),
Expr::False(_) => optimize_stmt(*stmt2, state, true),
Expr::True(_) => optimize_stmt(*stmt1, state, true),
expr => Stmt::IfElse(
Box::new(optimize_expr(expr, changed)),
Box::new(optimize_stmt(*stmt1, changed, true)),
match optimize_stmt(*stmt2, changed, true) {
Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*stmt1, state, true)),
match optimize_stmt(*stmt2, state, true) {
stmt if stmt.is_noop() => None,
stmt => Some(Box::new(stmt)),
},
@ -51,47 +111,51 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
Stmt::While(expr, stmt) => match *expr {
Expr::False(pos) => {
*changed = true;
state.set_dirty();
Stmt::Noop(pos)
}
Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed, false))),
Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))),
expr => Stmt::While(
Box::new(optimize_expr(expr, changed)),
Box::new(optimize_stmt(*stmt, changed, false)),
Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*stmt, state, false)),
),
},
Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed, false))),
Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))),
Stmt::For(id, expr, stmt) => Stmt::For(
id,
Box::new(optimize_expr(*expr, changed)),
Box::new(optimize_stmt(*stmt, changed, false)),
Box::new(optimize_expr(*expr, state)),
Box::new(optimize_stmt(*stmt, state, false)),
),
Stmt::Let(id, Some(expr), pos) => {
Stmt::Let(id, Some(Box::new(optimize_expr(*expr, changed))), pos)
Stmt::Let(id, Some(Box::new(optimize_expr(*expr, state))), pos)
}
Stmt::Let(_, None, _) => stmt,
Stmt::Block(statements, pos) => {
let orig_len = statements.len();
let orig_constants_len = state.constants.len();
let mut result: Vec<_> = statements
.into_iter() // For each statement
.rev() // Scan in reverse
.map(|s| optimize_stmt(s, changed, preserve_result)) // Optimize the statement
.map(|stmt| {
if let Stmt::Const(name, value, pos) = stmt {
state.push_constant(&name, *value);
state.set_dirty();
Stmt::Noop(pos) // No need to keep constants
} else {
optimize_stmt(stmt, state, preserve_result) // Optimize the statement
}
})
.enumerate()
.filter(|(i, s)| s.is_op() || (preserve_result && *i == 0)) // Remove no-op's but leave the last one if we need the result
.map(|(_, s)| s)
.rev()
.filter(|(i, stmt)| stmt.is_op() || (preserve_result && *i == orig_len - 1)) // Remove no-op's but leave the last one if we need the result
.map(|(_, stmt)| stmt)
.collect();
// Remove all raw expression statements that are pure except for the very last statement
let last_stmt = if preserve_result { result.pop() } else { None };
result.retain(|stmt| match stmt {
Stmt::Expr(expr) if expr.is_pure() => false,
_ => true,
});
result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure()));
if let Some(stmt) = last_stmt {
result.push(stmt);
@ -106,7 +170,6 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
match expr {
Stmt::Let(_, None, _) => removed = true,
Stmt::Let(_, Some(val_expr), _) if val_expr.is_pure() => removed = true,
_ => {
result.push(expr);
break;
@ -123,59 +186,82 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
.into_iter()
.rev()
.enumerate()
.map(|(i, s)| optimize_stmt(s, changed, i == 0)) // Optimize all other statements again
.map(|(i, s)| optimize_stmt(s, state, i == 0)) // Optimize all other statements again
.rev()
.collect();
}
*changed = *changed || orig_len != result.len();
if orig_len != result.len() {
state.set_dirty();
}
state.restore_constants(orig_constants_len);
match result[..] {
// No statements in block - change to No-op
[] => {
*changed = true;
state.set_dirty();
Stmt::Noop(pos)
}
// Only one statement - promote
[_] => {
*changed = true;
state.set_dirty();
result.remove(0)
}
_ => Stmt::Block(result, pos),
}
}
Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, changed))),
Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, state))),
Stmt::ReturnWithVal(Some(expr), is_return, pos) => Stmt::ReturnWithVal(
Some(Box::new(optimize_expr(*expr, changed))),
is_return,
pos,
),
Stmt::ReturnWithVal(Some(expr), is_return, pos) => {
Stmt::ReturnWithVal(Some(Box::new(optimize_expr(*expr, state))), is_return, pos)
}
stmt => stmt,
}
}
fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr {
match expr {
Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, changed, true) {
Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) {
Stmt::Noop(_) => {
*changed = true;
state.set_dirty();
Expr::Unit(pos)
}
Stmt::Expr(expr) => {
*changed = true;
state.set_dirty();
*expr
}
stmt => Expr::Stmt(Box::new(stmt), pos),
},
Expr::Assignment(id, expr, pos) => {
Expr::Assignment(id, Box::new(optimize_expr(*expr, changed)), pos)
Expr::Assignment(id1, expr1, pos1) => match *expr1 {
Expr::Assignment(id2, expr2, pos2) => match (*id1, *id2) {
(Expr::Variable(var1, _), Expr::Variable(var2, _)) if var1 == var2 => {
// Assignment to the same variable - fold
state.set_dirty();
Expr::Assignment(
Box::new(Expr::Variable(var1, pos1)),
Box::new(optimize_expr(*expr2, state)),
pos1,
)
}
(id1, id2) => Expr::Assignment(
Box::new(id1),
Box::new(Expr::Assignment(
Box::new(id2),
Box::new(optimize_expr(*expr2, state)),
pos2,
)),
pos1,
),
},
expr => Expr::Assignment(id1, Box::new(optimize_expr(expr, state)), pos1),
},
Expr::Dot(lhs, rhs, pos) => Expr::Dot(
Box::new(optimize_expr(*lhs, changed)),
Box::new(optimize_expr(*rhs, changed)),
Box::new(optimize_expr(*lhs, state)),
Box::new(optimize_expr(*rhs, state)),
pos,
),
@ -184,19 +270,25 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
(Expr::Array(mut items, _), Expr::IntegerConstant(i, _))
if i >= 0 && (i as usize) < items.len() && items.iter().all(|x| x.is_pure()) =>
{
// Array where everything is a pure - promote the indexed item.
// Array literal where everything is pure - promote the indexed item.
// All other items can be thrown away.
*changed = true;
state.set_dirty();
items.remove(i as usize)
}
(Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _))
if i >= 0 && (i as usize) < s.chars().count() =>
{
// String literal indexing - get the character
state.set_dirty();
Expr::CharConstant(s.chars().nth(i as usize).expect("should get char"), pos)
}
(lhs, rhs) => Expr::Index(
Box::new(optimize_expr(lhs, changed)),
Box::new(optimize_expr(rhs, changed)),
Box::new(optimize_expr(lhs, state)),
Box::new(optimize_expr(rhs, state)),
pos,
),
},
#[cfg(feature = "no_index")]
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
#[cfg(not(feature = "no_index"))]
Expr::Array(items, pos) => {
@ -204,95 +296,164 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
let items: Vec<_> = items
.into_iter()
.map(|expr| optimize_expr(expr, changed))
.map(|expr| optimize_expr(expr, state))
.collect();
*changed = *changed || orig_len != items.len();
if orig_len != items.len() {
state.set_dirty();
}
Expr::Array(items, pos)
}
#[cfg(feature = "no_index")]
Expr::Array(_, _) => panic!("encountered an array during no_index!"),
Expr::And(lhs, rhs) => match (*lhs, *rhs) {
(Expr::True(_), rhs) => {
*changed = true;
state.set_dirty();
rhs
}
(Expr::False(pos), _) => {
*changed = true;
state.set_dirty();
Expr::False(pos)
}
(lhs, Expr::True(_)) => {
*changed = true;
state.set_dirty();
lhs
}
(lhs, rhs) => Expr::And(
Box::new(optimize_expr(lhs, changed)),
Box::new(optimize_expr(rhs, changed)),
Box::new(optimize_expr(lhs, state)),
Box::new(optimize_expr(rhs, state)),
),
},
Expr::Or(lhs, rhs) => match (*lhs, *rhs) {
(Expr::False(_), rhs) => {
*changed = true;
state.set_dirty();
rhs
}
(Expr::True(pos), _) => {
*changed = true;
state.set_dirty();
Expr::True(pos)
}
(lhs, Expr::False(_)) => {
*changed = true;
state.set_dirty();
lhs
}
(lhs, rhs) => Expr::Or(
Box::new(optimize_expr(lhs, changed)),
Box::new(optimize_expr(rhs, changed)),
Box::new(optimize_expr(lhs, state)),
Box::new(optimize_expr(rhs, state)),
),
},
// Do not optimize anything within `dump_ast`
Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST => {
Expr::FunctionCall(id, args, def_value, pos)
}
// Actually call function to optimize it
Expr::FunctionCall(id, args, def_value, pos)
if id != KEYWORD_DEBUG // not debug
&& id != KEYWORD_PRINT // not print
&& state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations
&& args.iter().all(|expr| expr.is_constant()) // all arguments are constants
=>
{
let engine = state.engine.expect("engine should be Some");
let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect();
let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect();
engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r|
r.or(def_value.clone()).and_then(|result| map_dynamic_to_expr(result, pos).0)
.map(|expr| {
state.set_dirty();
expr
})).flatten()
.unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos))
}
// Optimize the function call arguments
Expr::FunctionCall(id, args, def_value, pos) => {
let orig_len = args.len();
let args: Vec<_> = args
.into_iter()
.map(|a| optimize_expr(a, changed))
.collect();
let args: Vec<_> = args.into_iter().map(|a| optimize_expr(a, state)).collect();
*changed = *changed || orig_len != args.len();
if orig_len != args.len() {
state.set_dirty();
}
Expr::FunctionCall(id, args, def_value, pos)
}
Expr::Variable(ref name, _) if state.contains_constant(name) => {
state.set_dirty();
// Replace constant with value
state
.find_constant(name)
.expect("should find constant in scope!")
.clone()
}
expr => expr,
}
}
pub(crate) fn optimize(statements: Vec<Stmt>) -> Vec<Stmt> {
pub(crate) fn optimize<'a>(
statements: Vec<Stmt>,
engine: Option<&Engine<'a>>,
scope: &Scope,
) -> Vec<Stmt> {
// If optimization level is None then skip optimizing
if engine
.map(|eng| eng.optimization_level == OptimizationLevel::None)
.unwrap_or(false)
{
return statements;
}
// Set up the state
let mut state = State::new();
state.engine = engine;
scope
.iter()
.filter(|ScopeEntry { var_type, expr, .. }| {
// Get all the constants with definite constant expressions
*var_type == VariableType::Constant
&& expr.as_ref().map(Expr::is_constant).unwrap_or(false)
})
.for_each(|ScopeEntry { name, expr, .. }| {
state.push_constant(
name.as_ref(),
expr.as_ref().expect("should be Some(expr)").clone(),
)
});
let orig_constants_len = state.constants.len();
// Optimization loop
let mut result = statements;
loop {
let mut changed = false;
state.reset();
state.restore_constants(orig_constants_len);
let num_statements = result.len();
result = result
.into_iter()
.rev() // Scan in reverse
.enumerate()
.map(|(i, stmt)| {
if let Stmt::Const(name, value, _) = &stmt {
// Load constants
state.push_constant(name, value.as_ref().clone());
stmt // Keep it in the top scope
} else {
// Keep all variable declarations at this level
let keep = stmt.is_var();
// and always keep the last return value
let keep = stmt.is_var() || i == num_statements - 1;
// Always keep the last return value
optimize_stmt(stmt, &mut changed, keep || i == 0)
optimize_stmt(stmt, &mut state, keep)
}
})
.rev()
.collect();
if !changed {
if !state.is_dirty() {
break;
}
}
@ -301,10 +462,7 @@ pub(crate) fn optimize(statements: Vec<Stmt>) -> Vec<Stmt> {
let last_stmt = result.pop();
// Remove all pure statements at top level
result.retain(|stmt| match stmt {
Stmt::Expr(expr) if expr.is_pure() => false,
_ => true,
});
result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure()));
if let Some(stmt) = last_stmt {
result.push(stmt); // Add back the last statement
@ -312,3 +470,32 @@ pub(crate) fn optimize(statements: Vec<Stmt>) -> Vec<Stmt> {
result
}
pub fn optimize_ast(
engine: &Engine,
scope: &Scope,
statements: Vec<Stmt>,
functions: Vec<FnDef>,
) -> AST {
AST(
match engine.optimization_level {
OptimizationLevel::None => statements,
OptimizationLevel::Simple => optimize(statements, None, &scope),
OptimizationLevel::Full => optimize(statements, Some(engine), &scope),
},
functions
.into_iter()
.map(|mut fn_def| {
match engine.optimization_level {
OptimizationLevel::None => (),
OptimizationLevel::Simple | OptimizationLevel::Full => {
let pos = fn_def.body.position();
let mut body = optimize(vec![fn_def.body], None, &Scope::new());
fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
}
}
Arc::new(fn_def)
})
.collect(),
)
}

View File

@ -1,8 +1,12 @@
//! Main module defining the lexer and parser.
use crate::any::Dynamic;
use crate::any::{Any, AnyExt, Dynamic};
use crate::engine::Engine;
use crate::error::{LexError, ParseError, ParseErrorType};
use crate::optimize::optimize;
use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize_ast;
use std::{
borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc,
@ -22,6 +26,7 @@ pub type INT = i64;
pub type INT = i32;
/// The system floating-point type
#[cfg(not(feature = "no_float"))]
pub type FLOAT = f64;
type LERR = LexError;
@ -143,12 +148,9 @@ impl fmt::Debug for Position {
/// Compiled AST (abstract syntax tree) of a Rhai script.
#[derive(Debug, Clone)]
pub struct AST(
pub(crate) Vec<Stmt>,
#[cfg(not(feature = "no_function"))] pub(crate) Vec<Arc<FnDef>>,
);
pub struct AST(pub(crate) Vec<Stmt>, pub(crate) Vec<Arc<FnDef>>);
#[derive(Debug)] // Do not derive Clone because it is expensive
#[derive(Debug, Clone)]
pub struct FnDef {
pub name: String,
pub params: Vec<String>,
@ -179,6 +181,7 @@ pub enum Stmt {
Loop(Box<Stmt>),
For(String, Box<Expr>, Box<Stmt>),
Let(String, Option<Box<Expr>>, Position),
Const(String, Box<Expr>, Position),
Block(Vec<Stmt>, Position),
Expr(Box<Expr>),
Break(Position),
@ -187,30 +190,22 @@ pub enum Stmt {
impl Stmt {
pub fn is_noop(&self) -> bool {
match self {
Stmt::Noop(_) => true,
_ => false,
}
matches!(self, Stmt::Noop(_))
}
pub fn is_op(&self) -> bool {
match self {
Stmt::Noop(_) => false,
_ => true,
}
!matches!(self, Stmt::Noop(_))
}
pub fn is_var(&self) -> bool {
match self {
Stmt::Let(_, _, _) => true,
_ => false,
}
matches!(self, Stmt::Let(_, _, _))
}
pub fn position(&self) -> Position {
match self {
Stmt::Noop(pos)
| Stmt::Let(_, _, pos)
| Stmt::Const(_, _, pos)
| Stmt::Block(_, pos)
| Stmt::Break(pos)
| Stmt::ReturnWithVal(_, _, pos) => *pos,
@ -225,14 +220,17 @@ pub enum Expr {
IntegerConstant(INT, Position),
#[cfg(not(feature = "no_float"))]
FloatConstant(FLOAT, Position),
Identifier(String, Position),
Variable(String, Position),
Property(String, Position),
CharConstant(char, Position),
StringConstant(String, Position),
Stmt(Box<Stmt>, Position),
FunctionCall(String, Vec<Expr>, Option<Dynamic>, Position),
Assignment(Box<Expr>, Box<Expr>, Position),
Dot(Box<Expr>, Box<Expr>, Position),
#[cfg(not(feature = "no_index"))]
Index(Box<Expr>, Box<Expr>, Position),
#[cfg(not(feature = "no_index"))]
Array(Vec<Expr>, Position),
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
@ -242,27 +240,73 @@ pub enum Expr {
}
impl Expr {
pub fn get_constant_value(&self) -> Dynamic {
match self {
Expr::IntegerConstant(i, _) => i.into_dynamic(),
Expr::CharConstant(c, _) => c.into_dynamic(),
Expr::StringConstant(s, _) => s.into_dynamic(),
Expr::True(_) => true.into_dynamic(),
Expr::False(_) => false.into_dynamic(),
Expr::Unit(_) => ().into_dynamic(),
#[cfg(not(feature = "no_index"))]
Expr::Array(items, _) if items.iter().all(Expr::is_constant) => items
.iter()
.map(Expr::get_constant_value)
.collect::<Vec<_>>()
.into_dynamic(),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.into_dynamic(),
_ => panic!("cannot get value of non-constant expression"),
}
}
pub fn get_constant_str(&self) -> String {
match self {
Expr::IntegerConstant(i, _) => i.to_string(),
Expr::CharConstant(c, _) => c.to_string(),
Expr::StringConstant(_, _) => "string".to_string(),
Expr::True(_) => "true".to_string(),
Expr::False(_) => "false".to_string(),
Expr::Unit(_) => "()".to_string(),
#[cfg(not(feature = "no_index"))]
Expr::Array(items, _) if items.iter().all(Expr::is_constant) => "array".to_string(),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.to_string(),
_ => panic!("cannot get value of non-constant expression"),
}
}
pub fn position(&self) -> Position {
match self {
Expr::IntegerConstant(_, pos)
| Expr::Identifier(_, pos)
| Expr::CharConstant(_, pos)
| Expr::StringConstant(_, pos)
| Expr::Variable(_, pos)
| Expr::Property(_, pos)
| Expr::Stmt(_, pos)
| Expr::FunctionCall(_, _, _, pos)
| Expr::Array(_, pos)
| Expr::True(pos)
| Expr::False(pos)
| Expr::Unit(pos) => *pos,
Expr::Assignment(e, _, _)
| Expr::Dot(e, _, _)
| Expr::Index(e, _, _)
| Expr::And(e, _)
| Expr::Or(e, _) => e.position(),
Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => {
e.position()
}
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, pos) => *pos,
#[cfg(not(feature = "no_index"))]
Expr::Array(_, pos) => *pos,
#[cfg(not(feature = "no_index"))]
Expr::Index(e, _, _) => e.position(),
}
}
@ -271,9 +315,15 @@ impl Expr {
/// A pure expression has no side effects.
pub fn is_pure(&self) -> bool {
match self {
#[cfg(not(feature = "no_index"))]
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure),
Expr::And(x, y) | Expr::Or(x, y) | Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
expr => expr.is_constant() || expr.is_identifier(),
#[cfg(not(feature = "no_index"))]
Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(),
expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)),
}
}
@ -289,12 +339,9 @@ impl Expr {
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, _) => true,
_ => false,
}
}
pub fn is_identifier(&self) -> bool {
match self {
Expr::Identifier(_, _) => true,
#[cfg(not(feature = "no_index"))]
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
_ => false,
}
}
@ -312,7 +359,9 @@ pub enum Token {
RightBrace,
LeftParen,
RightParen,
#[cfg(not(feature = "no_index"))]
LeftBracket,
#[cfg(not(feature = "no_index"))]
RightBracket,
Plus,
UnaryPlus,
@ -328,6 +377,7 @@ pub enum Token {
True,
False,
Let,
Const,
If,
Else,
While,
@ -343,6 +393,7 @@ pub enum Token {
Or,
Ampersand,
And,
#[cfg(not(feature = "no_function"))]
Fn,
Break,
Return,
@ -386,7 +437,9 @@ impl Token {
RightBrace => "}",
LeftParen => "(",
RightParen => ")",
#[cfg(not(feature = "no_index"))]
LeftBracket => "[",
#[cfg(not(feature = "no_index"))]
RightBracket => "]",
Plus => "+",
UnaryPlus => "+",
@ -402,6 +455,7 @@ impl Token {
True => "true",
False => "false",
Let => "let",
Const => "const",
If => "if",
Else => "else",
While => "while",
@ -417,6 +471,7 @@ impl Token {
Or => "||",
Ampersand => "&",
And => "&&",
#[cfg(not(feature = "no_function"))]
Fn => "fn",
Break => "break",
Return => "return",
@ -456,8 +511,6 @@ impl Token {
// RightBrace | {expr} - expr not unary & is closing
LeftParen | // {-expr} - is unary
// RightParen | (expr) - expr not unary & is closing
LeftBracket | // [-expr] - is unary
// RightBracket | [expr] - expr not unary & is closing
Plus |
UnaryPlus |
Minus |
@ -501,6 +554,10 @@ impl Token {
In |
PowerOfAssign => true,
#[cfg(not(feature = "no_index"))]
LeftBracket => true, // [-expr] - is unary
// RightBracket | [expr] - expr not unary & is closing
_ => false,
}
}
@ -510,9 +567,12 @@ impl Token {
use self::Token::*;
match *self {
RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma
| Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo
| EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true,
RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan
| GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo
| Pipe | Or | Ampersand | And | PowerOf => true,
#[cfg(not(feature = "no_index"))]
RightBrace | RightBracket => true,
_ => false,
}
@ -822,13 +882,15 @@ impl<'a> TokenIterator<'a> {
}
}
let out: String = result.iter().collect();
let has_letter = result.iter().any(char::is_ascii_alphabetic);
let identifier: String = result.iter().collect();
return Some((
match out.as_str() {
match identifier.as_str() {
"true" => Token::True,
"false" => Token::False,
"let" => Token::Let,
"const" => Token::Const,
"if" => Token::If,
"else" => Token::Else,
"while" => Token::While,
@ -836,10 +898,15 @@ impl<'a> TokenIterator<'a> {
"break" => Token::Break,
"return" => Token::Return,
"throw" => Token::Throw,
"fn" => Token::Fn,
"for" => Token::For,
"in" => Token::In,
_ => Token::Identifier(out),
#[cfg(not(feature = "no_function"))]
"fn" => Token::Fn,
_ if has_letter => Token::Identifier(identifier),
_ => Token::LexError(LERR::MalformedIdentifier(identifier)),
},
pos,
));
@ -873,8 +940,12 @@ impl<'a> TokenIterator<'a> {
'}' => return Some((Token::RightBrace, pos)),
'(' => return Some((Token::LeftParen, pos)),
')' => return Some((Token::RightParen, pos)),
#[cfg(not(feature = "no_index"))]
'[' => return Some((Token::LeftBracket, pos)),
#[cfg(not(feature = "no_index"))]
']' => return Some((Token::RightBracket, pos)),
'+' => {
return Some((
match self.char_stream.peek() {
@ -1146,7 +1217,7 @@ pub fn lex(input: &str) -> TokenIterator<'_> {
}
}
fn get_precedence(token: &Token) -> i8 {
fn get_precedence(token: &Token) -> u8 {
match *token {
Token::Equals
| Token::PlusAssign
@ -1161,28 +1232,49 @@ fn get_precedence(token: &Token) -> i8 {
| Token::ModuloAssign
| Token::PowerOfAssign => 10,
Token::Or | Token::XOr | Token::Pipe => 11,
Token::Or | Token::XOr | Token::Pipe => 50,
Token::And | Token::Ampersand => 12,
Token::And | Token::Ampersand => 60,
Token::LessThan
| Token::LessThanEqualsTo
| Token::GreaterThan
| Token::GreaterThanEqualsTo
| Token::EqualsTo
| Token::NotEqualsTo => 15,
| Token::NotEqualsTo => 70,
Token::Plus | Token::Minus => 20,
Token::Plus | Token::Minus => 80,
Token::Divide | Token::Multiply | Token::PowerOf => 40,
Token::Divide | Token::Multiply | Token::PowerOf => 90,
Token::LeftShift | Token::RightShift => 50,
Token::LeftShift | Token::RightShift => 100,
Token::Modulo => 60,
Token::Modulo => 110,
Token::Period => 100,
Token::Period => 120,
_ => -1,
_ => 0,
}
}
fn is_bind_right(token: &Token) -> bool {
match *token {
Token::Equals
| Token::PlusAssign
| Token::MinusAssign
| Token::MultiplyAssign
| Token::DivideAssign
| Token::LeftShiftAssign
| Token::RightShiftAssign
| Token::AndAssign
| Token::OrAssign
| Token::XOrAssign
| Token::ModuloAssign
| Token::PowerOfAssign => true,
Token::Period => true,
_ => false,
}
}
@ -1360,10 +1452,10 @@ fn parse_ident_expr<'a>(
#[cfg(not(feature = "no_index"))]
Some(&(Token::LeftBracket, pos)) => {
input.next();
parse_index_expr(Box::new(Expr::Identifier(id, begin)), input, pos)
parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos)
}
Some(_) => Ok(Expr::Identifier(id, begin)),
None => Ok(Expr::Identifier(id, Position::eof())),
Some(_) => Ok(Expr::Variable(id, begin)),
None => Ok(Expr::Variable(id, Position::eof())),
}
}
@ -1522,37 +1614,75 @@ fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pars
}
fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result<Expr, ParseError> {
fn valid_assignment_chain(expr: &Expr) -> (bool, Position) {
fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option<ParseError> {
match expr {
Expr::Identifier(_, pos) => (true, *pos),
Expr::Variable(_, _) => {
assert!(is_top, "property expected but gets variable");
None
}
Expr::Property(_, _) => {
assert!(!is_top, "variable expected but gets property");
None
}
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) if idx_lhs.is_identifier() => (true, idx_lhs.position()),
Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) => {
assert!(is_top, "property expected but gets variable");
None
}
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) => (false, idx_lhs.position()),
Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) => {
assert!(!is_top, "variable expected but gets property");
None
}
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, pos) => Some(ParseError::new(
match idx_lhs.as_ref() {
Expr::Index(_, _, _) => ParseErrorType::AssignmentToCopy,
_ => ParseErrorType::AssignmentToInvalidLHS,
},
*pos,
)),
Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() {
Expr::Identifier(_, _) => valid_assignment_chain(dot_rhs),
Expr::Variable(_, _) if is_top => valid_assignment_chain(dot_rhs, false),
Expr::Property(_, _) if !is_top => valid_assignment_chain(dot_rhs, false),
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) if idx_lhs.is_identifier() => {
valid_assignment_chain(dot_rhs)
Expr::Index(idx_lhs, _, _)
if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) && is_top =>
{
valid_assignment_chain(dot_rhs, false)
}
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) => (false, idx_lhs.position()),
Expr::Index(idx_lhs, _, _)
if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) && !is_top =>
{
valid_assignment_chain(dot_rhs, false)
}
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) => Some(ParseError::new(
ParseErrorType::AssignmentToCopy,
idx_lhs.position(),
)),
_ => (false, dot_lhs.position()),
expr => panic!("unexpected dot expression {:#?}", expr),
},
_ => (false, expr.position()),
_ => Some(ParseError::new(
ParseErrorType::AssignmentToInvalidLHS,
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)),
match valid_assignment_chain(&lhs, true) {
None => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)),
Some(err) => Err(err),
}
}
@ -1573,39 +1703,47 @@ fn parse_op_assignment(
fn parse_binary_op<'a>(
input: &mut Peekable<TokenIterator<'a>>,
precedence: i8,
parent_precedence: u8,
lhs: Expr,
) -> Result<Expr, ParseError> {
let mut current_lhs = lhs;
loop {
let mut current_precedence = -1;
let (current_precedence, bind_right) = if let Some(&(ref current_op, _)) = input.peek() {
(get_precedence(current_op), is_bind_right(current_op))
} else {
(0, false)
};
if let Some(&(ref current_op, _)) = input.peek() {
current_precedence = get_precedence(current_op);
}
if current_precedence < precedence {
// Bind left to the parent lhs expression if precedence is higher
// If same precedence, then check if the operator binds right
if current_precedence < parent_precedence
|| (current_precedence == parent_precedence && !bind_right)
{
return Ok(current_lhs);
}
if let Some((op_token, pos)) = input.next() {
input.peek();
let mut rhs = parse_unary(input)?;
let rhs = parse_unary(input)?;
let mut next_precedence = -1;
let next_precedence = if let Some(&(ref next_op, _)) = input.peek() {
get_precedence(next_op)
} else {
0
};
if let Some(&(ref next_op, _)) = input.peek() {
next_precedence = get_precedence(next_op);
}
if current_precedence < next_precedence {
rhs = parse_binary_op(input, current_precedence + 1, rhs)?;
} else if current_precedence >= 100 {
// Always bind right to left for precedence over 100
rhs = parse_binary_op(input, current_precedence, rhs)?;
}
// Bind to right if the next operator has higher precedence
// If same precedence, then check if the operator binds right
let rhs = if (current_precedence == next_precedence && bind_right)
|| current_precedence < next_precedence
{
parse_binary_op(input, current_precedence, rhs)?
} else {
// Otherwise bind to left (even if next operator has the same precedence)
rhs
};
current_lhs = match op_token {
Token::Plus => Expr::FunctionCall("+".into(), vec![current_lhs, rhs], None, pos),
@ -1619,7 +1757,29 @@ fn parse_binary_op<'a>(
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),
Token::Period => {
fn change_var_to_property(expr: Expr) -> Expr {
match expr {
Expr::Dot(lhs, rhs, pos) => Expr::Dot(
Box::new(change_var_to_property(*lhs)),
Box::new(change_var_to_property(*rhs)),
pos,
),
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx, pos) => {
Expr::Index(Box::new(change_var_to_property(*lhs)), idx, pos)
}
Expr::Variable(s, pos) => Expr::Property(s, pos),
expr => expr,
}
}
Expr::Dot(
Box::new(current_lhs),
Box::new(change_var_to_property(rhs)),
pos,
)
}
// Comparison operators default to false when passed invalid operands
Token::EqualsTo => Expr::FunctionCall(
@ -1696,7 +1856,7 @@ fn parse_binary_op<'a>(
fn parse_expr<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
let lhs = parse_unary(input)?;
parse_binary_op(input, 0, lhs)
parse_binary_op(input, 1, lhs)
}
fn parse_if<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
@ -1709,9 +1869,10 @@ fn parse_if<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseEr
Some(&(Token::Else, _)) => {
input.next();
let else_body = match input.peek() {
Some(&(Token::If, _)) => parse_if(input)?,
_ => parse_block(input)?,
let else_body = if matches!(input.peek(), Some(&(Token::If, _))) {
parse_if(input)?
} else {
parse_block(input)?
};
Ok(Stmt::IfElse(
@ -1746,14 +1907,17 @@ fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
let name = match input.next() {
Some((Token::Identifier(s), _)) => s,
Some((_, pos)) => return Err(ParseError::new(PERR::VarExpectsIdentifier, pos)),
None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())),
Some((Token::LexError(s), pos)) => {
return Err(ParseError::new(PERR::BadInput(s.to_string()), pos))
}
Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)),
None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())),
};
match input.next() {
Some((Token::In, _)) => {}
Some((_, pos)) => return Err(ParseError::new(PERR::VarExpectsIdentifier, pos)),
None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())),
Some((Token::In, _)) => (),
Some((_, pos)) => return Err(ParseError::new(PERR::MissingIn, pos)),
None => return Err(ParseError::new(PERR::MissingIn, Position::eof())),
}
let expr = parse_expr(input)?;
@ -1763,7 +1927,10 @@ fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
Ok(Stmt::For(name, Box::new(expr), Box::new(body)))
}
fn parse_var<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
fn parse_var<'a>(
input: &mut Peekable<TokenIterator<'a>>,
var_type: VariableType,
) -> Result<Stmt, ParseError> {
let pos = match input.next() {
Some((_, tok_pos)) => tok_pos,
_ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())),
@ -1771,17 +1938,31 @@ fn parse_var<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
let name = match input.next() {
Some((Token::Identifier(s), _)) => s,
Some((_, pos)) => return Err(ParseError::new(PERR::VarExpectsIdentifier, pos)),
None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())),
Some((Token::LexError(s), pos)) => {
return Err(ParseError::new(PERR::BadInput(s.to_string()), pos))
}
Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)),
None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())),
};
match input.peek() {
Some(&(Token::Equals, _)) => {
if matches!(input.peek(), Some(&(Token::Equals, _))) {
input.next();
let init_value = parse_expr(input)?;
Ok(Stmt::Let(name, Some(Box::new(init_value)), pos))
match var_type {
VariableType::Normal => Ok(Stmt::Let(name, Some(Box::new(init_value)), pos)),
VariableType::Constant if init_value.is_constant() => {
Ok(Stmt::Const(name, Box::new(init_value), pos))
}
_ => Ok(Stmt::Let(name, None, pos)),
// Constants require a constant expression
VariableType::Constant => Err(ParseError(
PERR::ForbiddenConstantExpr(name.to_string()),
init_value.position(),
)),
}
} else {
Ok(Stmt::Let(name, None, pos))
}
}
@ -1796,6 +1977,8 @@ fn parse_block<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Pars
match input.peek() {
Some(&(Token::RightBrace, _)) => (), // empty block
#[cfg(not(feature = "no_function"))]
Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)),
_ => {
@ -1849,7 +2032,7 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
let return_type = match token {
Token::Return => ReturnType::Return,
Token::Throw => ReturnType::Exception,
_ => panic!("unexpected token!"),
_ => panic!("token should be return or throw"),
};
input.next();
@ -1867,7 +2050,8 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
}
}
Some(&(Token::LeftBrace, _)) => parse_block(input),
Some(&(Token::Let, _)) => parse_var(input),
Some(&(Token::Let, _)) => parse_var(input, VariableType::Normal),
Some(&(Token::Const, _)) => parse_var(input, VariableType::Constant),
_ => parse_expr_stmt(input),
}
}
@ -1900,11 +2084,10 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
let mut params = Vec::new();
match input.peek() {
Some(&(Token::RightParen, _)) => {
if matches!(input.peek(), Some(&(Token::RightParen, _))) {
input.next();
}
_ => loop {
} else {
loop {
match input.next() {
Some((Token::RightParen, _)) => break,
Some((Token::Comma, _)) => (),
@ -1928,7 +2111,7 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
))
}
}
},
}
}
let body = parse_block(input)?;
@ -1941,13 +2124,10 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
})
}
fn parse_top_level<'a>(
fn parse_top_level<'a, 'e>(
input: &mut Peekable<TokenIterator<'a>>,
optimize_ast: bool,
) -> Result<AST, ParseError> {
) -> Result<(Vec<Stmt>, Vec<FnDef>), ParseError> {
let mut statements = Vec::<Stmt>::new();
#[cfg(not(feature = "no_function"))]
let mut functions = Vec::<FnDef>::new();
while input.peek().is_some() {
@ -1971,30 +2151,79 @@ fn parse_top_level<'a>(
}
}
return Ok(AST(
if optimize_ast {
optimize(statements)
} else {
statements
},
#[cfg(not(feature = "no_function"))]
functions
.into_iter()
.map(|mut fn_def| {
if optimize_ast {
let pos = fn_def.body.position();
let mut body = optimize(vec![fn_def.body]);
fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
}
Arc::new(fn_def)
})
.collect(),
));
Ok((statements, functions))
}
pub fn parse<'a>(
pub fn parse<'a, 'e>(
input: &mut Peekable<TokenIterator<'a>>,
optimize_ast: bool,
engine: &Engine<'e>,
scope: &Scope,
) -> Result<AST, ParseError> {
parse_top_level(input, optimize_ast)
let (statements, functions) = parse_top_level(input)?;
Ok(
#[cfg(not(feature = "no_optimize"))]
optimize_ast(engine, scope, statements, functions),
#[cfg(feature = "no_optimize")]
AST(statements, functions.into_iter().map(Arc::new).collect()),
)
}
pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option<Expr>, Dynamic) {
if value.is::<INT>() {
let value2 = value.clone();
(
Some(Expr::IntegerConstant(
*value.downcast::<INT>().expect("value should be INT"),
pos,
)),
value2,
)
} else if value.is::<char>() {
let value2 = value.clone();
(
Some(Expr::CharConstant(
*value.downcast::<char>().expect("value should be char"),
pos,
)),
value2,
)
} else if value.is::<String>() {
let value2 = value.clone();
(
Some(Expr::StringConstant(
*value.downcast::<String>().expect("value should be String"),
pos,
)),
value2,
)
} else if value.is::<bool>() {
let value2 = value.clone();
(
Some(
if *value.downcast::<bool>().expect("value should be bool") {
Expr::True(pos)
} else {
Expr::False(pos)
},
),
value2,
)
} else {
#[cfg(not(feature = "no_float"))]
{
if value.is::<FLOAT>() {
let value2 = value.clone();
return (
Some(Expr::FloatConstant(
*value.downcast::<FLOAT>().expect("value should be FLOAT"),
pos,
)),
value2,
);
}
}
(None, value)
}
}

View File

@ -41,6 +41,8 @@ pub enum EvalAltResult {
ErrorVariableNotFound(String, Position),
/// Assignment to an inappropriate LHS (left-hand-side) expression.
ErrorAssignmentToUnknownLHS(Position),
/// Assignment to a constant variable.
ErrorAssignmentToConstant(String, Position),
/// Returned type is not the same as the required output type.
/// Wrapped value is the type of the actual result.
ErrorMismatchOutputType(String, Position),
@ -59,10 +61,10 @@ pub enum EvalAltResult {
Return(Dynamic, Position),
}
impl Error for EvalAltResult {
fn description(&self) -> &str {
impl EvalAltResult {
pub(crate) fn desc(&self) -> &str {
match self {
Self::ErrorParsing(p) => p.description(),
Self::ErrorParsing(p) => p.desc(),
Self::ErrorFunctionNotFound(_, _) => "Function not found",
Self::ErrorFunctionArgsMismatch(_, _, _, _) => {
"Function call with wrong number of arguments"
@ -89,6 +91,7 @@ impl Error for EvalAltResult {
Self::ErrorAssignmentToUnknownLHS(_) => {
"Assignment to an unsupported left-hand side expression"
}
Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable",
Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect",
Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file",
Self::ErrorDotExpr(_, _) => "Malformed dot expression",
@ -98,15 +101,13 @@ impl Error for EvalAltResult {
Self::Return(_, _) => "[Not Error] Function returns value",
}
}
}
fn cause(&self) -> Option<&dyn Error> {
None
}
}
impl Error for EvalAltResult {}
impl fmt::Display for EvalAltResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let desc = self.description();
let desc = self.desc();
match self {
Self::ErrorFunctionNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
@ -116,6 +117,7 @@ impl fmt::Display for EvalAltResult {
Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos),
Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos),
Self::ErrorDotExpr(_, pos) => write!(f, "{} ({})", desc, pos),
@ -213,6 +215,7 @@ impl EvalAltResult {
| Self::ErrorFor(pos)
| Self::ErrorVariableNotFound(_, pos)
| Self::ErrorAssignmentToUnknownLHS(pos)
| Self::ErrorAssignmentToConstant(_, pos)
| Self::ErrorMismatchOutputType(_, pos)
| Self::ErrorDotExpr(_, pos)
| Self::ErrorArithmetic(_, pos)
@ -238,6 +241,7 @@ impl EvalAltResult {
| Self::ErrorFor(ref mut pos)
| Self::ErrorVariableNotFound(_, ref mut pos)
| Self::ErrorAssignmentToUnknownLHS(ref mut pos)
| Self::ErrorAssignmentToConstant(_, ref mut pos)
| Self::ErrorMismatchOutputType(_, ref mut pos)
| Self::ErrorDotExpr(_, ref mut pos)
| Self::ErrorArithmetic(_, ref mut pos)

View File

@ -1,11 +1,33 @@
//! Module that defines the `Scope` type representing a function call-stack scope.
use crate::any::{Any, Dynamic};
use crate::parser::{map_dynamic_to_expr, Expr, Position};
use std::borrow::Cow;
/// A type containing information about current scope.
/// Useful for keeping state between `Engine` runs.
/// Type of a variable in the Scope.
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
pub enum VariableType {
/// Normal variable.
Normal,
/// Immutable constant value.
Constant,
}
/// An entry in the Scope.
pub struct ScopeEntry<'a> {
/// Name of the variable.
pub name: Cow<'a, str>,
/// Type of the variable.
pub var_type: VariableType,
/// Current value of the variable.
pub value: Dynamic,
/// A constant expression if the initial value matches one of the recognized types.
pub expr: Option<Expr>,
}
/// A type containing information about the current scope.
/// Useful for keeping state between `Engine` evaluation runs.
///
/// # Example
///
@ -25,7 +47,7 @@ use std::borrow::Cow;
///
/// When searching for variables, newly-added variables are found before similarly-named but older variables,
/// allowing for automatic _shadowing_ of variables.
pub struct Scope<'a>(Vec<(Cow<'a, str>, Dynamic)>);
pub struct Scope<'a>(Vec<ScopeEntry<'a>>);
impl<'a> Scope<'a> {
/// Create a new Scope.
@ -44,18 +66,67 @@ impl<'a> Scope<'a> {
}
/// Add (push) a new variable to the Scope.
pub fn push<K: Into<Cow<'a, str>>, T: Any>(&mut self, key: K, value: T) {
self.0.push((key.into(), Box::new(value)));
pub fn push<K: Into<Cow<'a, str>>, T: Any>(&mut self, name: K, value: T) {
let value = value.into_dynamic();
// Map into constant expressions
//let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry {
name: name.into(),
var_type: VariableType::Normal,
value,
expr: None,
});
}
/// Add (push) a new variable to the Scope.
pub(crate) fn push_dynamic<K: Into<Cow<'a, str>>>(&mut self, key: K, value: Dynamic) {
self.0.push((key.into(), value));
/// Add (push) a new constant to the Scope.
///
/// Constants are immutable and cannot be assigned to. Their values never change.
/// Constants propagation is a technique used to optimize an AST.
/// However, in order to be used for optimization, constants must be in one of the recognized types:
/// `INT` (default to `i64`, `i32` if `only_i32`), `f64`, `String`, `char` and `bool`.
pub fn push_constant<K: Into<Cow<'a, str>>, T: Any>(&mut self, name: K, value: T) {
let value = value.into_dynamic();
// Map into constant expressions
let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry {
name: name.into(),
var_type: VariableType::Constant,
value,
expr,
});
}
/// Add (push) a new variable with a `Dynamic` value to the Scope.
pub(crate) fn push_dynamic<K: Into<Cow<'a, str>>>(
&mut self,
name: K,
var_type: VariableType,
value: Dynamic,
) {
let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry {
name: name.into(),
var_type,
value,
expr,
});
}
/// Remove (pop) the last variable from the Scope.
pub fn pop(&mut self) -> Option<(String, Dynamic)> {
self.0.pop().map(|(key, value)| (key.to_string(), value))
pub fn pop(&mut self) -> Option<(String, VariableType, Dynamic)> {
self.0.pop().map(
|ScopeEntry {
name,
var_type,
value,
..
}| (name.to_string(), var_type, value),
)
}
/// Truncate (rewind) the Scope to a previous size.
@ -64,13 +135,23 @@ impl<'a> Scope<'a> {
}
/// Find a variable in the Scope, starting from the last.
pub fn get(&self, key: &str) -> Option<(usize, &str, Dynamic)> {
pub fn get(&self, key: &str) -> Option<(usize, &str, VariableType, Dynamic)> {
self.0
.iter()
.enumerate()
.rev() // Always search a Scope in reverse order
.find(|(_, (name, _))| name == key)
.map(|(i, (name, value))| (i, name.as_ref(), value.clone()))
.find(|(_, ScopeEntry { name, .. })| name == key)
.map(
|(
i,
ScopeEntry {
name,
var_type,
value,
..
},
)| (i, name.as_ref(), *var_type, value.clone()),
)
}
/// Get the value of a variable in the Scope, starting from the last.
@ -79,53 +160,50 @@ impl<'a> Scope<'a> {
.iter()
.enumerate()
.rev() // Always search a Scope in reverse order
.find(|(_, (name, _))| name == key)
.and_then(|(_, (_, value))| value.downcast_ref::<T>())
.map(|value| value.clone())
.find(|(_, ScopeEntry { name, .. })| name == key)
.and_then(|(_, ScopeEntry { value, .. })| value.downcast_ref::<T>())
.map(T::clone)
}
/// Get a mutable reference to a variable in the Scope.
pub(crate) fn get_mut(&mut self, key: &str, index: usize) -> &mut Dynamic {
pub(crate) fn get_mut(&mut self, name: &str, index: usize) -> &mut Dynamic {
let entry = self.0.get_mut(index).expect("invalid index in Scope");
assert_eq!(entry.0, key, "incorrect key at Scope entry");
assert_ne!(
entry.var_type,
VariableType::Constant,
"get mut of constant variable"
);
assert_eq!(entry.name, name, "incorrect key at Scope entry");
&mut entry.1
&mut entry.value
}
/// Get a mutable reference to a variable in the Scope and downcast it to a specific type
#[cfg(not(feature = "no_index"))]
pub(crate) fn get_mut_by_type<T: Any + Clone>(&mut self, key: &str, index: usize) -> &mut T {
self.get_mut(key, index)
pub(crate) fn get_mut_by_type<T: Any + Clone>(&mut self, name: &str, index: usize) -> &mut T {
self.get_mut(name, index)
.downcast_mut::<T>()
.expect("wrong type cast")
}
/// Get an iterator to variables in the Scope.
pub fn iter(&self) -> impl Iterator<Item = (&str, &Dynamic)> {
self.0
.iter()
.rev() // Always search a Scope in reverse order
.map(|(key, value)| (key.as_ref(), value))
pub fn iter(&self) -> impl Iterator<Item = &ScopeEntry> {
self.0.iter().rev() // Always search a Scope in reverse order
}
}
/*
/// Get a mutable iterator to variables in the Scope.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut Dynamic)> {
self.0
.iter_mut()
.rev() // Always search a Scope in reverse order
.map(|(key, value)| (key.as_ref(), value))
}
*/
}
impl<'a, K> std::iter::Extend<(K, Dynamic)> for Scope<'a>
impl<'a, K> std::iter::Extend<(K, VariableType, Dynamic)> for Scope<'a>
where
K: Into<Cow<'a, str>>,
{
fn extend<T: IntoIterator<Item = (K, Dynamic)>>(&mut self, iter: T) {
fn extend<T: IntoIterator<Item = (K, VariableType, Dynamic)>>(&mut self, iter: T) {
self.0
.extend(iter.into_iter().map(|(key, value)| (key.into(), value)));
.extend(iter.into_iter().map(|(name, var_type, value)| ScopeEntry {
name: name.into(),
var_type,
value,
expr: None,
}));
}
}

View File

@ -7,6 +7,10 @@ fn test_arrays() -> Result<(), EvalAltResult> {
assert_eq!(engine.eval::<INT>("let x = [1, 2, 3]; x[1]")?, 2);
assert_eq!(engine.eval::<INT>("let y = [1, 2, 3]; y[1] = 5; y[1]")?, 5);
assert_eq!(
engine.eval::<char>(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?,
'3'
);
Ok(())
}
@ -48,10 +52,12 @@ fn test_array_with_structs() -> Result<(), EvalAltResult> {
assert_eq!(
engine.eval::<INT>(
"let a = [new_ts()]; \
a[0].x = 100; \
a[0].update(); \
a[0].x",
r"
let a = [new_ts()];
a[0].x = 100;
a[0].update();
a[0].x
"
)?,
1100
);

View File

@ -1,25 +1,24 @@
#![cfg(not(feature = "no_stdlib"))]
#![cfg(not(feature = "no_function"))]
use rhai::{Engine, EvalAltResult, INT};
#[test]
fn test_engine_call_fn() -> Result<(), EvalAltResult> {
fn test_call_fn() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
engine.consume(
true,
r"
fn hello(x, y) {
x.len() + y
x + y
}
fn hello(x) {
x * 2
}
",
true,
)?;
let r: i64 = engine.call_fn("hello", (String::from("abc"), 123 as INT))?;
assert_eq!(r, 126);
let r: i64 = engine.call_fn("hello", (42 as INT, 123 as INT))?;
assert_eq!(r, 165);
let r: i64 = engine.call_fn("hello", 123 as INT)?;
assert_eq!(r, 246);

View File

@ -11,12 +11,12 @@ fn test_chars() -> Result<(), EvalAltResult> {
{
assert_eq!(engine.eval::<char>(r#"let x="hello"; x[2]"#)?, 'l');
assert_eq!(
engine.eval::<String>(r#"let x="hello"; x[2]='$'; x"#)?,
engine.eval::<String>(r#"let y="hello"; y[2]='$'; y"#)?,
"he$lo".to_string()
);
}
assert!(engine.eval::<char>("'\\uhello'").is_err());
assert!(engine.eval::<char>(r"'\uhello'").is_err());
assert!(engine.eval::<char>("''").is_err());
Ok(())

21
tests/constants.rs Normal file
View File

@ -0,0 +1,21 @@
use rhai::{Engine, EvalAltResult, INT};
#[test]
fn test_constant() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(engine.eval::<INT>("const x = 123; x")?, 123);
assert!(
matches!(engine.eval::<INT>("const x = 123; x = 42;").expect_err("expects error"),
EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
);
#[cfg(not(feature = "no_index"))]
assert!(
matches!(engine.eval::<INT>("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"),
EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
);
Ok(())
}

View File

@ -6,12 +6,9 @@ fn test_decrement() -> Result<(), EvalAltResult> {
assert_eq!(engine.eval::<INT>("let x = 10; x -= 7; x")?, 3);
let r = engine.eval::<String>("let s = \"test\"; s -= \"ing\"; s");
match r {
Err(EvalAltResult::ErrorFunctionNotFound(err, _)) if err == "- (string, string)" => (),
_ => panic!(),
}
assert!(matches!(engine
.eval::<String>(r#"let s = "test"; s -= "ing"; s"#)
.expect_err("expects error"), EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (string, string)"));
Ok(())
}

View File

@ -24,54 +24,76 @@ fn test_math() -> Result<(), EvalAltResult> {
{
#[cfg(not(feature = "only_i32"))]
{
match engine.eval::<INT>("(-9223372036854775808).abs()") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return overflow error: {:?}", r),
}
match engine.eval::<INT>("9223372036854775807 + 1") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return overflow error: {:?}", r),
}
match engine.eval::<INT>("-9223372036854775808 - 1") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return underflow error: {:?}", r),
}
match engine.eval::<INT>("9223372036854775807 * 9223372036854775807") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return overflow error: {:?}", r),
}
match engine.eval::<INT>("9223372036854775807 / 0") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return division by zero error: {:?}", r),
}
match engine.eval::<INT>("9223372036854775807 % 0") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return division by zero error: {:?}", r),
}
assert!(matches!(
engine
.eval::<INT>("(-9223372036854775808).abs()")
.expect_err("expects negation overflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("9223372036854775807 + 1")
.expect_err("expects overflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("-9223372036854775808 - 1")
.expect_err("expects underflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("9223372036854775807 * 9223372036854775807")
.expect_err("expects overflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("9223372036854775807 / 0")
.expect_err("expects division by zero"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("9223372036854775807 % 0")
.expect_err("expects division by zero"),
EvalAltResult::ErrorArithmetic(_, _)
));
}
#[cfg(feature = "only_i32")]
{
match engine.eval::<INT>("2147483647 + 1") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return overflow error: {:?}", r),
}
match engine.eval::<INT>("-2147483648 - 1") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return underflow error: {:?}", r),
}
match engine.eval::<INT>("2147483647 * 2147483647") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return overflow error: {:?}", r),
}
match engine.eval::<INT>("2147483647 / 0") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return division by zero error: {:?}", r),
}
match engine.eval::<INT>("2147483647 % 0") {
Err(EvalAltResult::ErrorArithmetic(_, _)) => (),
r => panic!("should return division by zero error: {:?}", r),
}
assert!(matches!(
engine
.eval::<INT>("2147483647 + 1")
.expect_err("expects overflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("-2147483648 - 1")
.expect_err("expects underflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("2147483647 * 2147483647")
.expect_err("expects overflow"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("2147483647 / 0")
.expect_err("expects division by zero"),
EvalAltResult::ErrorArithmetic(_, _)
));
assert!(matches!(
engine
.eval::<INT>("2147483647 % 0")
.expect_err("expects division by zero"),
EvalAltResult::ErrorArithmetic(_, _)
));
}
}

View File

@ -5,12 +5,10 @@ use rhai::{Engine, EvalAltResult, RegisterFn, INT};
fn test_mismatched_op() {
let mut engine = Engine::new();
let r = engine.eval::<INT>("60 + \"hello\"");
match r {
Err(EvalAltResult::ErrorMismatchOutputType(err, _)) if err == "string" => (),
_ => panic!(),
}
assert!(
matches!(engine.eval::<INT>(r#"60 + "hello""#).expect_err("expects error"),
EvalAltResult::ErrorMismatchOutputType(err, _) if err == "string")
);
}
#[test]
@ -30,15 +28,17 @@ fn test_mismatched_op_custom_type() {
engine.register_type_with_name::<TestStruct>("TestStruct");
engine.register_fn("new_ts", TestStruct::new);
let r = engine.eval::<INT>("60 + new_ts()");
let r = engine
.eval::<INT>("60 + new_ts()")
.expect_err("expects error");
match r {
#[cfg(feature = "only_i32")]
Err(EvalAltResult::ErrorFunctionNotFound(err, _)) if err == "+ (i32, TestStruct)" => (),
assert!(
matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)")
);
#[cfg(not(feature = "only_i32"))]
Err(EvalAltResult::ErrorFunctionNotFound(err, _)) if err == "+ (i64, TestStruct)" => (),
_ => panic!(),
}
assert!(
matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)")
);
}

32
tests/optimizer.rs Normal file
View File

@ -0,0 +1,32 @@
#![cfg(not(feature = "no_optimize"))]
use rhai::{Engine, EvalAltResult, OptimizationLevel, INT};
#[test]
fn test_optimizer() -> Result<(), EvalAltResult> {
fn run_test(engine: &mut Engine) -> Result<(), EvalAltResult> {
assert_eq!(engine.eval::<INT>(r"if true { 42 } else { 123 }")?, 42);
assert_eq!(
engine.eval::<INT>(r"if 1 == 1 || 2 > 3 { 42 } else { 123 }")?,
42
);
assert_eq!(
engine.eval::<INT>(r#"const abc = "hello"; if abc < "foo" { 42 } else { 123 }"#)?,
123
);
Ok(())
}
let mut engine = Engine::new();
engine.set_optimization_level(OptimizationLevel::None);
run_test(&mut engine)?;
engine.set_optimization_level(OptimizationLevel::Simple);
run_test(&mut engine)?;
engine.set_optimization_level(OptimizationLevel::Full);
run_test(&mut engine)?;
Ok(())
}

View File

@ -1,4 +1,10 @@
use rhai::{Engine, EvalAltResult, FLOAT, INT};
use rhai::{Engine, EvalAltResult, INT};
#[cfg(not(feature = "no_float"))]
use rhai::FLOAT;
#[cfg(not(feature = "no_float"))]
const EPSILON: FLOAT = 0.0000000001;
#[test]
fn test_power_of() -> Result<(), EvalAltResult> {
@ -9,9 +15,8 @@ fn test_power_of() -> Result<(), EvalAltResult> {
#[cfg(not(feature = "no_float"))]
{
assert_eq!(
engine.eval::<FLOAT>("2.2 ~ 3.3")?,
13.489468760533386 as FLOAT
assert!(
(engine.eval::<FLOAT>("2.2 ~ 3.3")? - 13.489468760533386 as FLOAT).abs() <= EPSILON
);
assert_eq!(engine.eval::<FLOAT>("2.0~-2.0")?, 0.25 as FLOAT);
assert_eq!(engine.eval::<FLOAT>("(-2.0~-2.0)")?, 0.25 as FLOAT);
@ -31,9 +36,9 @@ fn test_power_of_equals() -> Result<(), EvalAltResult> {
#[cfg(not(feature = "no_float"))]
{
assert_eq!(
engine.eval::<FLOAT>("let x = 2.2; x ~= 3.3; x")?,
13.489468760533386 as FLOAT
assert!(
(engine.eval::<FLOAT>("let x = 2.2; x ~= 3.3; x")? - 13.489468760533386 as FLOAT).abs()
<= EPSILON
);
assert_eq!(
engine.eval::<FLOAT>("let x = 2.0; x ~= -2.0; x")?,

View File

@ -1,18 +1,14 @@
use rhai::{Engine, EvalAltResult, INT};
use rhai::{Engine, EvalAltResult};
#[test]
fn test_throw() {
let mut engine = Engine::new();
match engine.eval::<INT>(r#"if true { throw "hello" }"#) {
Ok(_) => panic!("not an error"),
Err(EvalAltResult::ErrorRuntime(s, _)) if s == "hello" => (),
Err(err) => panic!("wrong error: {}", err),
}
assert!(matches!(
engine.eval::<()>(r#"if true { throw "hello" }"#).expect_err("expects error"),
EvalAltResult::ErrorRuntime(s, _) if s == "hello"));
match engine.eval::<INT>(r#"throw;"#) {
Ok(_) => panic!("not an error"),
Err(EvalAltResult::ErrorRuntime(s, _)) if s == "" => (),
Err(err) => panic!("wrong error: {}", err),
}
assert!(matches!(
engine.eval::<()>(r#"throw;"#).expect_err("expects error"),
EvalAltResult::ErrorRuntime(s, _) if s == ""));
}