Add Rhai book.
This commit is contained in:
64
doc/src/engine/call-fn.md
Normal file
64
doc/src/engine/call-fn.md
Normal 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
23
doc/src/engine/compile.md
Normal 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())?;
|
||||
```
|
25
doc/src/engine/expressions.md
Normal file
25
doc/src/engine/expressions.md
Normal 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
43
doc/src/engine/func.md
Normal 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))
|
||||
}));
|
||||
```
|
54
doc/src/engine/hello-world.md
Normal file
54
doc/src/engine/hello-world.md
Normal 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
109
doc/src/engine/optimize.md
Normal 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`].
|
24
doc/src/engine/optimize/disable.md
Normal file
24
doc/src/engine/optimize/disable.md
Normal 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);
|
||||
```
|
36
doc/src/engine/optimize/eager.md
Normal file
36
doc/src/engine/optimize/eager.md
Normal 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'
|
||||
```
|
24
doc/src/engine/optimize/optimize-levels.md
Normal file
24
doc/src/engine/optimize/optimize-levels.md
Normal 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);
|
||||
```
|
17
doc/src/engine/optimize/reoptimize.md
Normal file
17
doc/src/engine/optimize/reoptimize.md
Normal 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);
|
||||
```
|
43
doc/src/engine/optimize/semantics.md
Normal file
43
doc/src/engine/optimize/semantics.md
Normal 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`].
|
19
doc/src/engine/optimize/side-effects.md
Normal file
19
doc/src/engine/optimize/side-effects.md
Normal 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.
|
16
doc/src/engine/optimize/volatility.md
Normal file
16
doc/src/engine/optimize/volatility.md
Normal 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
24
doc/src/engine/raw.md
Normal 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` |
|
Reference in New Issue
Block a user