Refine README.

This commit is contained in:
Stephen Chung 2020-05-24 12:40:28 +08:00
parent 1798d4d6a0
commit 65ee262f1b

143
README.md
View File

@ -23,14 +23,14 @@ Rhai's current features set:
* Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to * Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to
one single source file, all with names starting with `"unsafe_"`). one single source file, all with names starting with `"unsafe_"`).
* Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature). * Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature).
* Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment without explicit permission. * Sand-boxed - the scripting [`Engine`], if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`).
* Rugged (protection against [stack-overflow](#maximum-call-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.). * Rugged (protection against [stack-overflow](#maximum-call-stack-depth) and [runaway scripts](#maximum-number-of-operations) etc.).
* Track script evaluation [progress](#tracking-progress) and manually terminate a script run. * Track script evaluation [progress](#tracking-progress) and manually terminate a script run.
* [`no-std`](#optional-features) support. * [`no-std`](#optional-features) support.
* [Function overloading](#function-overloading). * [Function overloading](#function-overloading).
* [Operator overloading](#operator-overloading). * [Operator overloading](#operator-overloading).
* Organize code base with dynamically-loadable [Modules]. * Organize code base with dynamically-loadable [Modules].
* Compiled script is [optimized](#script-optimization) for repeated evaluations. * Scripts are [optimized](#script-optimization) (useful for template-based machine-generated scripts) for repeated evaluations.
* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features). * Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features).
* Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/) * Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/)
to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are
@ -55,7 +55,7 @@ Use the latest released crate version on [`crates.io`](https::/crates.io/crates/
rhai = "*" rhai = "*"
``` ```
Crate versions are released on [`crates.io`](https::/crates.io/crates/rhai/) infrequently, so if you want to track the Crate versions are released on [`crates.io`](https::/crates.io/crates/rhai/) infrequently, so to track the
latest features, enhancements and bug fixes, pull directly from GitHub: latest features, enhancements and bug fixes, pull directly from GitHub:
```toml ```toml
@ -134,7 +134,7 @@ Omitting arrays (`no_index`) yields the most code-size savings, followed by floa
Disable script-defined functions (`no_function`) only when the feature is not needed because code size savings is minimal. Disable script-defined functions (`no_function`) only when the feature is not needed because code size savings is minimal.
[`Engine::new_raw`](#raw-engine) creates a _raw_ engine. [`Engine::new_raw`](#raw-engine) creates a _raw_ engine.
A _raw_ engine supports, out of the box, only a very restricted set of basic arithmetic and logical operators. A _raw_ engine supports, out of the box, only a very [restricted set](#built-in-operators) of basic arithmetic and logical operators.
Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint. Selectively include other necessary functionalities by loading specific [packages] to minimize the footprint.
Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once. Packages are sharable (even across threads via the [`sync`] feature), so they only have to be created once.
@ -168,7 +168,7 @@ Examples can be run with the following command:
cargo run --example name cargo run --example name
``` ```
The `repl` example is a particularly good one as it allows you to interactively try out Rhai's The `repl` example is a particularly good one as it allows one to interactively try out Rhai's
language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop).
Example Scripts Example Scripts
@ -379,6 +379,17 @@ In many controlled embedded environments, however, these are not needed.
Use `Engine::new_raw` to create a _raw_ `Engine`, in which only a minimal set of basic arithmetic and logical operators Use `Engine::new_raw` to create a _raw_ `Engine`, in which only a minimal set of basic arithmetic and logical operators
are supported. are supported.
### Built-in operators
| Operator | Supported for type (see [standard types]) |
| ---------------------------- | -------------------------------------------------------------------- |
| `+`, `-`, `*`, `/`, `%`, `~` | `INT`, `FLOAT` (if not [`no_float`]) |
| `<<`, `>>`, `^` | `INT` |
| `&`, `\|` | `INT`, `bool` |
| `&&`, `\|\|` | `bool` |
| `==`, `!=` | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `String` |
| `>`, `>=`, `<`, `<=` | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `String` |
### Packages ### Packages
[package]: #packages [package]: #packages
@ -459,6 +470,7 @@ Values and types
[`type_of()`]: #values-and-types [`type_of()`]: #values-and-types
[`to_string()`]: #values-and-types [`to_string()`]: #values-and-types
[`()`]: #values-and-types [`()`]: #values-and-types
[standard types]: #values-and-types
The following primitive types are supported natively: The following primitive types are supported natively:
@ -475,7 +487,7 @@ The following primitive types are supported natively:
| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | | **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ |
| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | | **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. |
| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | | **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. |
| **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` | `""` _(empty string)_ | | **Nothing/void/nil/null** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ |
All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - 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. they even cannot be added together. This is very similar to Rust.
@ -563,7 +575,7 @@ let value: i64 = item.cast(); // type can also be inferred
let value = item.try_cast::<i64>().unwrap(); // 'try_cast' does not panic when the cast fails, but returns 'None' let value = item.try_cast::<i64>().unwrap(); // 'try_cast' does not panic when the cast fails, but returns 'None'
``` ```
The `type_name` method gets the name of the actual type as a static string slice, which you may match against. The `type_name` method gets the name of the actual type as a static string slice, which can be `match`-ed against.
```rust ```rust
let list: Array = engine.eval("...")?; // return type is 'Array' let list: Array = engine.eval("...")?; // return type is 'Array'
@ -637,7 +649,7 @@ fn add(x: i64, y: i64) -> i64 {
// Function that returns a 'Dynamic' value - must return a 'Result' // Function that returns a 'Dynamic' value - must return a 'Result'
fn get_any_value() -> Result<Dynamic, Box<EvalAltResult>> { fn get_any_value() -> Result<Dynamic, Box<EvalAltResult>> {
Ok((42_i64).into()) // standard supported types can use 'into()' Ok((42_i64).into()) // standard types can use 'into()'
} }
fn main() -> Result<(), Box<EvalAltResult>> fn main() -> Result<(), Box<EvalAltResult>>
@ -662,12 +674,12 @@ fn main() -> Result<(), Box<EvalAltResult>>
``` ```
To create a [`Dynamic`] value, use the `Dynamic::from` method. To create a [`Dynamic`] value, use the `Dynamic::from` method.
Standard supported types in Rhai can also use `into()`. [Standard types] in Rhai can also use `into()`.
```rust ```rust
use rhai::Dynamic; use rhai::Dynamic;
let x = (42_i64).into(); // 'into()' works for standard supported types let x = (42_i64).into(); // 'into()' works for standard types
let y = Dynamic::from(String::from("hello!")); // remember &str is not supported by Rhai let y = Dynamic::from(String::from("hello!")); // remember &str is not supported by Rhai
``` ```
@ -805,7 +817,7 @@ See the [relevant section](#script-optimization) for more details.
Custom types and methods Custom types and methods
----------------------- -----------------------
Here's an more complete example of working with Rust. First the example, then we'll break it into parts: A more complete example of working with Rust:
```rust ```rust
use rhai::{Engine, EvalAltResult}; use rhai::{Engine, EvalAltResult};
@ -843,8 +855,8 @@ fn main() -> Result<(), Box<EvalAltResult>>
} }
``` ```
All custom types must implement `Clone`. This allows the [`Engine`] to pass by value. All custom types must implement `Clone` as this allows the [`Engine`] to pass by value.
You can turn off support for custom types via the [`no_object`] feature. Support for custom types can be turned off via the [`no_object`] feature.
```rust ```rust
#[derive(Clone)] #[derive(Clone)]
@ -853,11 +865,12 @@ 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, create a few methods for later use in scripts.
Notice that the custom type needs to be _registered_ with the [`Engine`].
```rust ```rust
impl TestStruct { impl TestStruct {
fn update(&mut self) { fn update(&mut self) { // methods take &mut as first parameter
self.field += 41; self.field += 41;
} }
@ -871,19 +884,19 @@ let engine = Engine::new();
engine.register_type::<TestStruct>(); engine.register_type::<TestStruct>();
``` ```
To use native types, methods and functions with the [`Engine`], we need to register them. To use native types, methods and functions with the [`Engine`], simply register them using one of the `Engine::register_XXX` API.
There are some convenience functions to help with these. Below, the `update` and `new` methods are registered with the [`Engine`]. Below, the `update` and `new` methods are registered using `Engine::register_fn`.
*Note: [`Engine`] follows the convention that methods use a `&mut` first parameter so that invoking methods ***Note**: Rhai follows the convention that methods of custom types take a `&mut` first parameter so that invoking methods
can update the value in memory.* can update the custom types. All other parameters in Rhai are passed by value (i.e. clones).*
```rust ```rust
engine.register_fn("update", TestStruct::update); // registers 'update(&mut TestStruct)' engine.register_fn("update", TestStruct::update); // registers 'update(&mut TestStruct)'
engine.register_fn("new_ts", TestStruct::new); // registers 'new()' engine.register_fn("new_ts", TestStruct::new); // registers 'new()'
``` ```
Finally, we call our script. The script can see the function and method we registered earlier. The custom type is then ready for us in scripts. Scripts can see the functions and methods registered earlier.
We need to get the result back out from script land just as before, this time casting to our custom struct type. Get the evaluation result back out from script-land just as before, this time casting to the custom type:
```rust ```rust
let result = engine.eval::<TestStruct>("let x = new_ts(); x.update(); x")?; let result = engine.eval::<TestStruct>("let x = new_ts(); x.update(); x")?;
@ -891,18 +904,19 @@ let result = engine.eval::<TestStruct>("let x = new_ts(); x.update(); x")?;
println!("result: {}", result.field); // prints 42 println!("result: {}", result.field); // prints 42
``` ```
In fact, any function with a first argument (either by copy or via a `&mut` reference) can be used as a method call In fact, any function with a first argument that is a `&mut` reference can be used as method calls because
on that type because internally they are the same thing: internally they are the same thing: methods on a type is implemented as a functions taking a `&mut` first argument.
methods on a type is implemented as a functions taking a `&mut` first argument.
```rust ```rust
fn foo(ts: &mut TestStruct) -> i64 { fn foo(ts: &mut TestStruct) -> i64 {
ts.field ts.field
} }
engine.register_fn("foo", foo); engine.register_fn("foo", foo); // register ad hoc function with correct signature
let result = engine.eval::<i64>("let x = new_ts(); x.foo()")?; let result = engine.eval::<i64>(
"let x = new_ts(); x.foo()" // 'foo' can be called like a method on 'x'
)?;
println!("result: {}", result); // prints 1 println!("result: {}", result); // prints 1
``` ```
@ -916,7 +930,7 @@ let result = engine.eval::<i64>("let x = [1, 2, 3]; x.len()")?;
``` ```
[`type_of()`] works fine with custom types and returns the name of the type. [`type_of()`] works fine with custom types and returns the name of the type.
If `register_type_with_name` is used to register the custom type If `Engine::register_type_with_name` is used to register the custom type
with a special "pretty-print" name, [`type_of()`] will return that name instead. with a special "pretty-print" name, [`type_of()`] will return that name instead.
```rust ```rust
@ -1238,7 +1252,7 @@ number = -5 - +5;
Numeric functions Numeric functions
----------------- -----------------
The following standard functions (defined in the [`BasicMathPackage`] but excluded if using a [raw `Engine`]) operate on The following standard functions (defined in the [`BasicMathPackage`](#packages) but excluded if using a [raw `Engine`]) operate on
`i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only:
| Function | Description | | Function | Description |
@ -1701,10 +1715,10 @@ if now.elapsed() > 30.0 {
Comparison operators Comparison operators
-------------------- --------------------
Comparing most values of the same data type work out-of-the-box for standard types supported by the system. Comparing most values of the same data type work out-of-the-box for all [standard types] supported by the system.
However, if using a [raw `Engine`], comparisons can only be made between restricted system types - However, if using a [raw `Engine`] without loading any [packages], comparisons can only be made between a limited
`INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), [string], [array], `bool`, `char`. set of types (see [built-in operators](#built-in-operators)).
```rust ```rust
42 == 42; // true 42 == 42; // true
@ -2269,7 +2283,7 @@ so that it does not consume more resources that it is allowed to.
The most important resources to watch out for are: The most important resources to watch out for are:
* **Memory**: A malignant script may continuously grow an [array] or [object map] until all memory is consumed. * **Memory**: A malignant script may continuously grow an [array] or [object map] until all memory is consumed.
It may also create a large [array] or [objecct map] literal that exhausts all memory during parsing. It may also create a large [array] or [object map] literal that exhausts all memory during parsing.
* **CPU**: A malignant script may run an infinite tight loop that consumes all CPU cycles. * **CPU**: A malignant script may run an infinite tight loop that consumes all CPU cycles.
* **Time**: A malignant script may run indefinitely, thereby blocking the calling system which is waiting for a result. * **Time**: A malignant script may run indefinitely, thereby blocking the calling system which is waiting for a result.
* **Stack**: A malignant script may attempt an infinite recursive call that exhausts the call stack. * **Stack**: A malignant script may attempt an infinite recursive call that exhausts the call stack.
@ -2459,7 +2473,7 @@ For example, in the following:
123; // eliminated: no effect 123; // eliminated: no effect
"hello"; // eliminated: no effect "hello"; // eliminated: no effect
[1, 2, x, x*2, 5]; // eliminated: no effect [1, 2, x, x*2, 5]; // eliminated: no effect
foo(42); // NOT eliminated: the function 'foo' may have side effects foo(42); // NOT eliminated: the function 'foo' may have side-effects
666 // NOT eliminated: this is the return value of the block, 666 // NOT eliminated: this is the return value of the block,
// and the block is the last one so this is the return value of the whole script // and the block is the last one so this is the return value of the whole script
} }
@ -2510,7 +2524,7 @@ if DECISION == 1 { // NOT optimized away because you can define
} }
``` ```
because no operator functions will be run (in order not to trigger side effects) during the optimization process 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: (unless the optimization level is set to [`OptimizationLevel::Full`]). So, instead, do this:
```rust ```rust
@ -2532,7 +2546,7 @@ if DECISION_1 {
In general, boolean constants are most effective for the optimizer to automatically prune In general, boolean constants are most effective for the optimizer to automatically prune
large `if`-`else` branches because they do not depend on operators. large `if`-`else` branches because they do not depend on operators.
Alternatively, turn the optimizer to [`OptimizationLevel::Full`] Alternatively, turn the optimizer to [`OptimizationLevel::Full`].
Here be dragons! Here be dragons!
================ ================
@ -2548,7 +2562,7 @@ There are actually three levels of optimizations: `None`, `Simple` and `Full`.
* `None` is obvious - no optimization on the AST is performed. * `None` is obvious - no optimization on the AST is performed.
* `Simple` (default) performs relatively _safe_ optimizations without causing side effects * `Simple` (default) performs only relatively _safe_ optimizations without causing side-effects
(i.e. it only relies on static analysis and will not actually perform any function calls). (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. * `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
@ -2603,28 +2617,32 @@ let x = (1+2)*3-4/5%6; // <- will be replaced by 'let x = 9'
let y = (1>2) || (3<=4); // <- will be replaced by 'let y = true' let y = (1>2) || (3<=4); // <- will be replaced by 'let y = true'
``` ```
Function side effect considerations Side-effect considerations
---------------------------------- --------------------------
All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state
nor cause side any effects, with the exception of `print` and `debug` which are handled specially) so using nor cause any side-effects, with the exception of `print` and `debug` which are handled specially) so using
[`OptimizationLevel::Full`] is usually quite safe _unless_ you register your own types and functions. [`OptimizationLevel::Full`] is usually quite safe _unless_ custom types and functions are registered.
If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block). If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block).
If custom functions are registered to replace built-in operators, they will also be called when the operators are used If custom functions are registered to overload built-in operators, they will also be called when the operators are used
(in an `if` statement, for example) and cause side-effects. (in an `if` statement, for example) causing side-effects.
Function volatility considerations Therefore, the rule-of-thumb is: _always_ register custom types and functions _after_ compiling scripts if
--------------------------------- [`OptimizationLevel::Full`] is used. _DO NOT_ depend on knowledge that the functions have no side-effects,
because those functions can change later on and, when that happens, existing scripts may break in subtle ways.
Even if a custom function does not mutate state nor cause side effects, it may still be _volatile_, i.e. it _depends_ Volatility considerations
on the external environment and is not _pure_. A perfect example is a function that gets the current time - -------------------------
obviously each run will return a different value! The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that
all functions are _pure_, so when it finds constant arguments it will eagerly execute the function call.
This causes the script to behave differently from the intended semantics because essentially the result of the function call
will always be the same value.
Therefore, **avoid using [`OptimizationLevel::Full`]** if you intend to register non-_pure_ custom types and/or functions. Even if a custom function does not mutate state nor cause side-effects, it may still be _volatile_,
i.e. it _depends_ on the external environment and is not _pure_.
A perfect example is a function that gets the current time - obviously each run will return a different value!
The optimizer, when using [`OptimizationLevel::Full`], will _merrily assume_ that all functions are _pure_,
so when it finds constant arguments (or none) it eagerly executes the function call and replaces it with the result.
This causes the script to behave differently from the intended semantics.
Therefore, **avoid using [`OptimizationLevel::Full`]** if non-_pure_ custom types and/or functions are involved.
Subtle semantic changes Subtle semantic changes
----------------------- -----------------------
@ -2659,7 +2677,7 @@ print("end!");
In the script above, if `my_decision` holds anything other than a boolean value, the script should have been terminated due to 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 (because an access to `my_decision` produces a type error. 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. no side-effects), thus the script silently runs to completion without errors.
Turning off optimizations Turning off optimizations
------------------------- -------------------------
@ -2674,6 +2692,8 @@ let engine = rhai::Engine::new();
engine.set_optimization_level(rhai::OptimizationLevel::None); engine.set_optimization_level(rhai::OptimizationLevel::None);
``` ```
Alternatively, turn off optimizations via the [`no_optimize`] feature.
`eval` - or "How to Shoot Yourself in the Foot even Easier" `eval` - or "How to Shoot Yourself in the Foot even Easier"
--------------------------------------------------------- ---------------------------------------------------------
@ -2722,8 +2742,8 @@ x += 32;
print(x); print(x);
``` ```
For those who subscribe to the (very sensible) motto of ["`eval` is **evil**"](http://linterrors.com/js/eval-is-evil), For those who subscribe to the (very sensible) motto of ["`eval` is evil"](http://linterrors.com/js/eval-is-evil),
disable `eval` by overriding it, probably with something that throws. disable `eval` by overloading it, probably with something that throws.
```rust ```rust
fn eval(script) { throw "eval is evil! I refuse to run " + script } fn eval(script) { throw "eval is evil! I refuse to run " + script }
@ -2731,7 +2751,7 @@ fn eval(script) { throw "eval is evil! I refuse to run " + script }
let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2" let x = eval("40 + 2"); // 'eval' here throws "eval is evil! I refuse to run 40 + 2"
``` ```
Or override it from Rust: Or overload it from Rust:
```rust ```rust
fn alt_eval(script: String) -> Result<(), Box<EvalAltResult>> { fn alt_eval(script: String) -> Result<(), Box<EvalAltResult>> {
@ -2741,4 +2761,15 @@ fn alt_eval(script: String) -> Result<(), Box<EvalAltResult>> {
engine.register_result_fn("eval", alt_eval); engine.register_result_fn("eval", alt_eval);
``` ```
There is even a [package] named `EvalPackage` which implements the disabling override. There is even a package named [`EvalPackage`](#packages) which implements the disabling override:
```rust
use rhai::Engine;
use rhai::packages::Package // load the 'Package' trait to use packages
use rhai::packages::EvalPackage; // the 'eval' package disables 'eval'
let mut engine = Engine::new();
let package = EvalPackage::new(); // create the package
engine.load_package(package.get()); // load the package
```