Add Rhai book.

This commit is contained in:
Stephen Chung
2020-06-20 12:06:17 +08:00
parent 7e80d62df5
commit c7f1e12d6a
101 changed files with 3827 additions and 0 deletions

64
doc/src/engine/call-fn.md Normal file
View File

@@ -0,0 +1,64 @@
Calling Rhai Functions from Rust
===============================
{{#include ../links.md}}
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function
from Rust via `Engine::call_fn`.
Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]).
```rust
// Define functions in a script.
let ast = engine.compile(true,
r#"
// a function with two parameters: string and i64
fn hello(x, y) {
x.len + y
}
// functions can be overloaded: this one takes only one parameter
fn hello(x) {
x * 2
}
// this one takes no parameters
fn hello() {
42
}
// this one is private and cannot be called by 'call_fn'
private hidden() {
throw "you shouldn't see me!";
}
"#)?;
// A custom scope can also contain any variables/constants available to the functions
let mut scope = Scope::new();
// Evaluate a function defined in the script, passing arguments into the script as a tuple.
// Beware, arguments must be of the correct types because Rhai does not have built-in type conversions.
// If arguments of the wrong types are passed, the Engine will not find the function.
let result: i64 = engine.call_fn(&mut scope, &ast, "hello", ( String::from("abc"), 123_i64 ) )?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// put arguments in a tuple
let result: i64 = engine.call_fn(&mut scope, &ast, "hello", (123_i64,) )?;
// ^^^^^^^^^^ tuple of one
let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?;
// ^^ unit = tuple of zero
// The following call will return a function-not-found error because
// 'hidden' is declared with 'private'.
let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?;
```
For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`, passing it
anything that implements `IntoIterator<Item = Dynamic>` (such as a simple `Vec<Dynamic>`):
```rust
let result: Dynamic = engine.call_fn_dynamic(&mut scope, &ast, "hello",
vec![ String::from("abc").into(), 123_i64.into() ])?;
```

23
doc/src/engine/compile.md Normal file
View File

@@ -0,0 +1,23 @@
Compile a Script (to AST)
========================
{{#include ../links.md}}
To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form:
```rust
// Compile to an AST and store it for later evaluations
let ast = engine.compile("40 + 2")?;
for _ in 0..42 {
let result: i64 = engine.eval_ast(&ast)?;
println!("Answer #{}: {}", i, result); // prints 42
}
```
Compiling a script file is also supported (not available under [`no_std`] or in [WASM] builds):
```rust
let ast = engine.compile_file("hello_world.rhai".into())?;
```

View File

@@ -0,0 +1,25 @@
Evaluate Expressions Only
========================
{{#include ../links.md}}
Sometimes a use case does not require a full-blown scripting _language_, but only needs to evaluate _expressions_.
In these cases, use the `Engine::compile_expression` and `Engine::eval_expression` methods or their `_with_scope` variants.
```rust
let result = engine.eval_expression::<i64>("2 + (10 + 10) * 2")?;
```
When evaluating _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignment -
is supported and will be considered parse errors when encountered.
```rust
// The following are all syntax errors because the script is not an expression.
engine.eval_expression::<()>("x = 42")?;
let ast = engine.compile_expression("let x = 42")?;
let result = engine.eval_expression_with_scope::<i64>(&mut scope, "if x { 42 } else { 123 }")?;
```

43
doc/src/engine/func.md Normal file
View File

@@ -0,0 +1,43 @@
Create a Rust Anonymous Function from a Rhai Function
===================================================
{{#include ../links.md}}
It is possible to further encapsulate a script in Rust such that it becomes a normal Rust function.
Such an _anonymous function_ is basically a boxed closure, very useful as call-back functions.
Creating them is accomplished via the `Func` trait which contains `create_from_script`
(as well as its companion method `create_from_ast`):
```rust
use rhai::{Engine, Func}; // use 'Func' for 'create_from_script'
let engine = Engine::new(); // create a new 'Engine' just for this
let script = "fn calc(x, y) { x + y.len < 42 }";
// Func takes two type parameters:
// 1) a tuple made up of the types of the script function's parameters
// 2) the return type of the script function
//
// 'func' will have type Box<dyn Fn(i64, String) -> Result<bool, Box<EvalAltResult>>> and is callable!
let func = Func::<(i64, String), bool>::create_from_script(
// ^^^^^^^^^^^^^ function parameter types in tuple
engine, // the 'Engine' is consumed into the closure
script, // the script, notice number of parameters must match
"calc" // the entry-point function name
)?;
func(123, "hello".to_string())? == false; // call the anonymous function
schedule_callback(func); // pass it as a callback to another function
// Although there is nothing you can't do by manually writing out the closure yourself...
let engine = Engine::new();
let ast = engine.compile(script)?;
schedule_callback(Box::new(move |x: i64, y: String| -> Result<bool, Box<EvalAltResult>> {
engine.call_fn(&mut Scope::new(), &ast, "calc", (x, y))
}));
```

View File

@@ -0,0 +1,54 @@
Hello World in Rhai
===================
{{#include ../links.md}}
To get going with Rhai is as simple as creating an instance of the scripting engine `rhai::Engine` via
`Engine::new`, then calling the `eval` method:
```rust
use rhai::{Engine, EvalAltResult};
fn main() -> Result<(), Box<EvalAltResult>>
{
let engine = Engine::new();
let result = engine.eval::<i64>("40 + 2")?;
// ^^^^^^^ cast the result to an 'i64', this is required
println!("Answer: {}", result); // prints 42
Ok(())
}
```
`rhai::EvalAltResult` is a Rust `enum` containing all errors encountered during the parsing or evaluation process.
Evaluate a Script
----------------
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.
Use [`Dynamic`] for uncertain return types.
There are two ways to specify the return type - _turbofish_ notation, or type inference.
```rust
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
result.is::<i64>() == true;
let result: Dynamic = engine.eval("boo()")?; // use 'Dynamic' if you're not sure what type it'll be!
let result = engine.eval::<String>("40 + 2")?; // returns an error because the actual return type is i64, not String
```
Evaluate a script file directly:
```rust
let result = engine.eval_file::<i64>("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf'
```

109
doc/src/engine/optimize.md Normal file
View File

@@ -0,0 +1,109 @@
Script Optimization
===================
{{#include ../links.md}}
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.
Dead Code Removal
----------------
For example, in the following:
```rust
{
let x = 999; // NOT eliminated: variable may be used later on (perhaps even an 'eval')
123; // eliminated: no effect
"hello"; // eliminated: no effect
[1, 2, x, x*2, 5]; // eliminated: no effect
foo(42); // NOT eliminated: the function 'foo' may have side-effects
666 // NOT eliminated: this is the return value of the block,
// and the block is the last one so this is the return value of the whole script
}
```
Rhai attempts to eliminate _dead code_ (i.e. code that does nothing, for example an expression by itself as a statement,
which is allowed in Rhai).
The above script optimizes to:
```rust
{
let x = 999;
foo(42);
666
}
```
Constants Propagation
--------------------
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
// 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.
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.
Watch Out for Function Calls
---------------------------
Beware, however, that most operators are actually function calls, and those functions can be overridden,
so they are not optimized away:
```rust
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 {
:
}
```
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
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`].

View File

@@ -0,0 +1,24 @@
Turn Off Script Optimizations
============================
{{#include ../../links.md}}
When scripts:
* are known to be run only _once_,
* are known to contain no dead code,
* do not use constants in calculations
the optimization pass may be a waste of time and resources. In that case, turn optimization off
by setting the optimization level to [`OptimizationLevel::None`].
Alternatively, turn off optimizations via the [`no_optimize`] feature.
```rust
let engine = rhai::Engine::new();
// Turn off the optimizer
engine.set_optimization_level(rhai::OptimizationLevel::None);
```

View File

@@ -0,0 +1,36 @@
Eager Function Evaluation When Using Full Optimization Level
==========================================================
{{#include ../../links.md}}
When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_
evaluated all function calls with constant arguments, using the result to replace the call.
This also applies to all operators (which are implemented as functions).
For instance, the same example above:
```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
}
print("hello!"); // <- the above is equivalent to this
// ('print' and 'debug' are handled specially)
```
Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result.
This does not happen with [`OptimizationLevel::Simple`] which doesn't assume all functions to be _pure_.
```rust
// When compiling the following with OptimizationLevel::Full...
let x = (1+2)*3-4/5%6; // <- will be replaced by 'let x = 9'
let y = (1>2) || (3<=4); // <- will be replaced by 'let y = true'
```

View File

@@ -0,0 +1,24 @@
Optimization Levels
==================
{{#include ../../links.md}}
Set Optimization Level
---------------------
There are actually three levels of optimizations: `None`, `Simple` and `Full`.
* `None` is obvious - no optimization on the AST is performed.
* `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).
* `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 `Engine::set_optimization_level`:
```rust
// Turn on aggressive optimizations
engine.set_optimization_level(rhai::OptimizationLevel::Full);
```

View File

@@ -0,0 +1,17 @@
Re-Optimize an AST
==================
{{#include ../../links.md}}
If it is ever needed to _re_-optimize an `AST`, use the `optimize_ast` method:
```rust
// Compile script to AST
let ast = engine.compile("40 + 2")?;
// Create a new 'Scope' - put constants in it to aid optimization if using 'OptimizationLevel::Full'
let scope = Scope::new();
// Re-optimize the AST
let ast = engine.optimize_ast(&scope, &ast, OptimizationLevel::Full);
```

View File

@@ -0,0 +1,43 @@
Subtle Semantic Changes After Optimization
=========================================
{{#include ../../links.md}}
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
```
If the original script were evaluated instead, it would have been an error - the variable `hello` does not 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
print("end!");
// The above optimizes to:
print("start!");
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 (because an access to `my_decision` produces
no side-effects), thus the script silently runs to completion without errors.
It is usually a _Very 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), turn script optimization off by setting the optimization level to [`OptimizationLevel::None`].

View File

@@ -0,0 +1,19 @@
Side-Effect Considerations for Full Optimization Level
====================================================
{{#include ../../links.md}}
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 any side-effects, with the exception of `print` and `debug` which are handled specially) so using
[`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 to overload built-in operators, they will also be called when the operators are used
(in an `if` statement, for example) causing side-effects.
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.

View File

@@ -0,0 +1,16 @@
Volatility Considerations for Full Optimization Level
===================================================
{{#include ../../links.md}}
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.

24
doc/src/engine/raw.md Normal file
View File

@@ -0,0 +1,24 @@
Raw `Engine`
===========
{{#include ../links.md}}
`Engine::new` creates a scripting [`Engine`] with common functionalities (e.g. printing to the console via `print`).
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
are supported.
Built-in Operators
------------------
| Operators | Assignment operators | Supported for type (see [standard types]) |
| ------------------------ | ---------------------------- | ----------------------------------------------------------------------------- |
| `+`, | `+=` | `INT`, `FLOAT` (if not [`no_float`]), `ImmutableString` |
| `-`, `*`, `/`, `%`, `~`, | `-=`, `*=`, `/=`, `%=`, `~=` | `INT`, `FLOAT` (if not [`no_float`]) |
| `<<`, `>>`, `^`, | `<<=`, `>>=`, `^=` | `INT` |
| `&`, `\|`, | `&=`, `\|=` | `INT`, `bool` |
| `&&`, `\|\|` | | `bool` |
| `==`, `!=` | | `INT`, `FLOAT` (if not [`no_float`]), `bool`, `char`, `()`, `ImmutableString` |
| `>`, `>=`, `<`, `<=` | | `INT`, `FLOAT` (if not [`no_float`]), `char`, `()`, `ImmutableString` |