reorganize module

This commit is contained in:
Timur Gordon
2025-04-04 08:28:07 +02:00
parent 1ea37e2e7f
commit 939b6b4e57
375 changed files with 7580 additions and 191 deletions

View File

@@ -1,212 +0,0 @@
Constants Propagation
=====================
{{#include ../../links.md}}
```admonish tip.side
Effective in template-based machine-generated scripts to turn on/off certain sections.
```
[Constants] propagation is commonly used to:
* remove dead code,
* avoid [variable] lookups,
* [pre-calculate](op-eval.md) [constant] expressions.
```rust
const ABC = true;
const X = 41;
if ABC || calc(X+1) { print("done!"); } // 'ABC' is constant so replaced by 'true'...
// 'X' is constant so replaced by 41...
if true || calc(42) { print("done!"); } // '41+1' is replaced by 42
// since '||' short-circuits, 'calc' 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
```
~~~admonish tip "Tip: Custom `Scope` constants"
[Constant] values can be provided in a custom [`Scope`] object to the [`Engine`]
for optimization purposes.
```rust
use rhai::{Engine, Scope};
let engine = Engine::new();
let mut scope = Scope::new();
// Add constant to custom scope
scope.push_constant("ABC", true);
// Evaluate script with custom scope
engine.run_with_scope(&mut scope,
r#"
if ABC { // 'ABC' is replaced by 'true'
print("done!");
}
"#)?;
```
~~~
~~~admonish tip "Tip: Customer module constants"
[Constants] defined in [modules] that are registered into an [`Engine`] via
`Engine::register_global_module` are used in optimization.
```rust
use rhai::{Engine, Module};
let mut engine = Engine::new();
let mut module = Module::new();
// Add constant to module
module.set_var("ABC", true);
// Register global module
engine.register_global_module(module.into());
// Evaluate script
engine.run(
r#"
if ABC { // 'ABC' is replaced by 'true'
print("done!");
}
"#)?;
```
~~~
~~~admonish danger "Caveat: Constants in custom scope and modules are also propagated into functions"
[Constants] defined at _global_ level typically cannot be seen by script [functions] because they are _pure_.
```rust
const MY_CONSTANT = 42; // <- constant defined at global level
print(MY_CONSTANT); // <- optimized to: print(42)
fn foo() {
MY_CONSTANT // <- not optimized: 'foo' cannot see 'MY_CONSTANT'
}
print(foo()); // error: 'MY_CONSTANT' not found
```
When [constants] are provided in a custom [`Scope`] (e.g. via `Engine::compile_with_scope`,
`Engine::eval_with_scope` or `Engine::run_with_scope`), or in a [module] registered via
`Engine::register_global_module`, instead of defined within the same script, they are also
propagated to [functions].
This is usually the intuitive usage and behavior expected by regular users, even though it means
that a script will behave differently (essentially a runtime error) when [script optimization] is disabled.
```rust
use rhai::{Engine, Scope};
let engine = Engine::new();
let mut scope = Scope::new();
// Add constant to custom scope
scope.push_constant("MY_CONSTANT", 42_i64);
engine.run_with_scope(&mut scope,
"
print(MY_CONSTANT); // optimized to: print(42)
fn foo() {
MY_CONSTANT // optimized to: fn foo() { 42 }
}
print(foo()); // prints 42
")?;
```
The script will act differently when [script optimization] is disabled because script [functions]
are _pure_ and typically cannot see [constants] within the custom [`Scope`].
Therefore, constants in [functions] now throw a runtime error.
```rust
use rhai::{Engine, Scope, OptimizationLevel};
let mut engine = Engine::new();
// Turn off script optimization, no constants propagation is performed
engine.set_optimization_level(OptimizationLevel::None);
let mut scope = Scope::new();
// Add constant to custom scope
scope.push_constant("MY_CONSTANT", 42_i64);
engine.run_with_scope(&mut scope,
"
print(MY_CONSTANT); // prints 42
fn foo() {
MY_CONSTANT // <- 'foo' cannot see 'MY_CONSTANT'
}
print(foo()); // error: 'MY_CONSTANT' not found
")?;
```
~~~
~~~admonish danger "Caveat: Beware of large constants"
[Constants] propagation replaces each usage of the [constant] with a clone of its value.
This may have negative implications to performance if the [constant] value is expensive to clone
(e.g. if the type is very large).
```rust
let mut scope = Scope::new();
// Push a large constant into the scope...
let big_type = AVeryLargeType::take_long_time_to_create();
scope.push_constant("MY_BIG_TYPE", big_type);
// Causes each usage of 'MY_BIG_TYPE' in the script below to be replaced
// by cloned copies of 'AVeryLargeType'.
let result = engine.run_with_scope(&mut scope,
"
let value = MY_BIG_TYPE.value;
let data = MY_BIG_TYPE.data;
let len = MY_BIG_TYPE.len();
let has_options = MY_BIG_TYPE.has_options();
let num_options = MY_BIG_TYPE.options_len();
")?;
```
To avoid this, compile the script first to an [`AST`] _without_ the [constants], then evaluate the
[`AST`] (e.g. with `Engine::eval_ast_with_scope` or `Engine::run_ast_with_scope`) together with
the [constants].
~~~
~~~admonish danger "Caveat: Constants may be modified by Rust methods"
If the [constants] are modified later on (yes, it is possible, via Rust _methods_),
the modified values will not show up in the optimized script.
Only the initialization values of [constants] are ever retained.
```rust
const MY_SECRET_ANSWER = 42;
MY_SECRET_ANSWER.update_to(666); // assume 'update_to(&mut i64)' is a Rust function
print(MY_SECRET_ANSWER); // prints 42 because the constant is propagated
```
This is almost never a problem because real-world scripts seldom modify a [constant],
but the possibility is always there.
~~~

View File

@@ -1,51 +0,0 @@
Dead Code Elimination
=====================
{{#include ../../links.md}}
```admonish question.side.wide "But who writes dead code?"
Nobody deliberately writes scripts with dead code (we hope).
They are, however, extremely common in template-based machine-generated scripts.
```
Rhai attempts to eliminate _dead code_.
"Dead code" is code that does nothing and has no side effects.
Example is an pure expression by itself as a statement (allowed in Rhai).
The result of the expression is calculated then immediately discarded and not used.
```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, 4]; // eliminated: no effect
if 42 > 0 { // '42 > 0' is replaced by 'true' and the first branch promoted
foo(42); // promoted, NOT eliminated: the function 'foo' may have side-effects
} else {
bar(x); // eliminated: branch is never reached
}
let z = x; // eliminated: local variable, no side-effects, and only pure afterwards
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
}
```
The above script optimizes to:
```rust
{
let x = 999;
foo(42);
666
}
```

View File

@@ -1,25 +0,0 @@
Turn Off Script Optimizations
=============================
{{#include ../../links.md}}
When scripts:
* are known to be run only _once_ and then thrown away,
* 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`].
```rust
let engine = rhai::Engine::new();
// Turn off the optimizer
engine.set_optimization_level(rhai::OptimizationLevel::None);
```
Alternatively, disable optimizations via the [`no_optimize`] feature.

View File

@@ -1,75 +0,0 @@
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).
```rust
// When compiling the following with OptimizationLevel::Full...
const DECISION = 1;
// this condition is now eliminated because 'sign(DECISION) > 0'
if sign(DECISION) > 0 // <- is a call to the 'sign' and '>' functions, and they return '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)
```
~~~admonish danger "Won't this be dangerous?"
Yes! _Very!_
```rust
// Nuclear silo control
if launch_nukes && president_okeyed {
print("This is NOT a drill!");
update_defcon(1);
start_world_war(3);
launch_all_nukes();
} else {
print("This is a drill. Thank you for your cooperation.");
}
```
In the script above (well... as if nuclear silos will one day be controlled by Rhai scripts),
the functions `update_defcon`, `start_world_war` and `launch_all_nukes` will be evaluated
during _compilation_ because they have constant arguments.
The [variables] `launch_nukes` and `president_okeyed` are never checked, because the script
actually has not yet been run! The functions are called during compilation.
This is, _obviously_, not what you want.
**Moral of the story: compile with an [`Engine`] that does not have any functions registered.
Register functions _AFTER_ compilation.**
~~~
~~~admonish question "Why would I ever want to do this then?"
Good question! There are two reasons:
* A function call may result in cleaner code than the resultant value.
In Rust, this would have been handled via a `const` function.
* Evaluating a value to a [custom type] that has no representation in script.
```rust
// A complex function that returns a unique ID based on the arguments
let id = make_unique_id(123, "hello", true);
// The above is arguably clearer than:
// let id = 835781293546; // generated from 123, "hello" and true
// A custom type that cannot be represented in script
let complex_obj = make_complex_obj(42);
```
~~~

View File

@@ -1,62 +0,0 @@
Script Optimization
===================
{{#title 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.
Optimization Levels
===================
{{#include ../../links.md}}
There are three levels of optimization: `None`, `Simple` and `Full`.
The default is `Simple`.
An [`Engine`]'s optimization level is set via [`Engine::set_optimization_level`][options].
```rust
// Turn on aggressive optimizations
engine.set_optimization_level(rhai::OptimizationLevel::Full);
```
`None`
------
`None` is obvious &ndash; no optimization on the AST is performed.
`Simple` (Default)
------------------
`Simple` performs only relatively _safe_ optimizations without causing side-effects (i.e. it only
relies on static analysis and [built-in operators] for [constant] [standard types], and will not
perform any external function calls).
```admonish warning.small
After _constants propagation_ is performed, if the [constants] are then modified (yes, it is possible, via Rust functions),
the modified values will _not_ show up in the optimized script.
Only the initialization values of [constants] are ever retained.
```
```admonish warning.small
Overriding a [built-in operator] in the [`Engine`] afterwards has no effect after the
optimizer replaces an expression with its calculated value.
```
`Full`
------
`Full` is _much_ more aggressive, _including_ calling external functions on [constant] arguments to
determine their results.
One benefit to this is that many more optimization opportunities arise, especially with regards to
comparison operators.

View File

@@ -1,85 +0,0 @@
Eager Operator Evaluation
=========================
{{#include ../../links.md}}
Most operators are actually function calls, and those functions can be overridden, so whether they
are optimized away depends on the situation:
```admonish info.side.wide "No external functions"
Rhai guarantees that no external function will be run, which may trigger side-effects
(unless the optimization level is [`OptimizationLevel::Full`]).
```
* if the operands are not [constant] values, it is **not** optimized;
* if the [operator] is [overloaded][operator overloading], it is **not** optimized because the
overloading function may not be _pure_ (i.e. may cause side-effects when called);
* if the [operator] is not _built-in_ (see list of [built-in operators]), it is **not** optimized;
* if the [operator] is a [built-in operator] for a [standard type][standard types], it is called and
replaced by a [constant] result.
```js
// The following is most likely generated by machine.
const DECISION = 1; // this is an integer, one of the standard types
if DECISION == 1 { // this is optimized into 'true'
:
} else if DECISION == 2 { // this is optimized into 'false'
:
} else if DECISION == 3 { // this is optimized into 'false'
:
} else {
:
}
// Or an equivalent using 'switch':
switch DECISION {
1 => ..., // this statement is promoted
2 => ..., // this statement is eliminated
3 => ..., // this statement is eliminated
_ => ... // this statement is eliminated
}
```
Pre-Evaluation of Constant Expressions
--------------------------------------
Because of the eager evaluation of [operators][built-in operators] for [standard types], many
[constant] expressions will be evaluated and replaced by the result.
```rust
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'
```
For operators that are not optimized away due to one of the above reasons, the function calls are
simply left behind.
```rust
// Assume 'new_state' returns some custom type that is NOT one of the standard types.
// Also assume that the '==' operator is defined for that custom type.
const DECISION_1 = new_state(1);
const DECISION_2 = new_state(2);
const DECISION_3 = new_state(3);
if DECISION == 1 { // NOT optimized away because the operator is not built-in
: // and may cause side-effects if called!
:
} else if DECISION == 2 { // same here, NOT optimized away
:
} else if DECISION == 3 { // same here, NOT optimized away
:
} else {
:
}
```
Alternatively, turn the optimizer to [`OptimizationLevel::Full`].

View File

@@ -1,21 +0,0 @@
Optimization Passes
===================
{{#include ../../links.md}}
[Script optimization] is performed via multiple _passes_.
Each pass does a specific optimization.
The optimization is completed when no passes can simplify the [`AST`] any further.
Built-in Optimization Passes
----------------------------
| Pass | Description |
| ------------------------------------------ | ------------------------------------------------- |
| [Dead code elimination](dead-code.md) | Eliminates code that cannot be reached |
| [Constants propagation](constants.md) | Replaces [constants] with values |
| [Compound assignments rewrite](rewrite.md) | Rewrites assignments into compound assignments |
| [Eager operator evaluation](op-eval.md) | Eagerly calls operators with [constant] arguments |
| [Eager function evaluation](eager.md) | Eagerly calls functions with [constant] arguments |

View File

@@ -1,50 +0,0 @@
Re-Optimize an AST
==================
{{#include ../../links.md}}
Sometimes it is more efficient to store one single, large script with delimited code blocks guarded by
constant variables. This script is compiled once to an [`AST`].
Then, depending on the execution environment, constants are passed into the [`Engine`] and the
[`AST`] is _re_-optimized based on those constants via `Engine::optimize_ast`, effectively pruning
out unused code sections.
The final, optimized [`AST`] is then used for evaluations.
```rust
// Compile master script to AST
let master_ast = engine.compile(
"
fn do_work() {
// Constants in scope are also propagated into functions
print(SCENARIO);
}
switch SCENARIO {
1 => do_work(),
2 => do_something(),
3 => do_something_else(),
_ => do_nothing()
}
")?;
for n in 0..5_i64 {
// Create a new scope - put constants in it to aid optimization
let mut scope = Scope::new();
scope.push_constant("SCENARIO", n);
// Re-optimize the AST
let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple);
// Run it
engine.run_ast(&new_ast)?;
}
```
```admonish note.small "Constants propagation"
Beware that [constants] inside the custom [`Scope`] will also be propagated to [functions] defined
within the script while normally such [functions] are _pure_ and cannot see [variables]/[constants]
within the global [`Scope`].
```

View File

@@ -1,47 +0,0 @@
Compound Assignment Rewrite
===========================
{{#include ../../links.md}}
```admonish info.side "Avoid cloning"
Arguments passed as value are always cloned.
```
Usually, a _compound assignment_ (e.g. `+=` for append) takes a mutable first parameter
(i.e. `&mut`) while the corresponding simple [operator] (i.e. `+`) does not.
The script optimizer rewrites normal assignments into _compound assignments_ wherever possible in
order to avoid unnecessary cloning.
```rust
let big = create_some_very_big_type();
big = big + 1;
// ^ 'big' is cloned here
// The above is equivalent to:
let temp_value = big + 1;
big = temp_value;
big += 1; // <- 'big' is NOT cloned
```
~~~admonish warning.small "Warning: Simple references only"
Only _simple variable references_ are optimized.
No [_common sub-expression elimination_](https://en.wikipedia.org/wiki/Common_subexpression_elimination)
is performed by Rhai.
```rust
x = x + 1; // <- this statement...
x += 1; // <- ... is rewritten to this
x[y] = x[y] + 1; // <- but this is not,
// so MUCH slower...
x[y] += 1; // <- ... than this
```
~~~

View File

@@ -1,85 +0,0 @@
Subtle Semantic Changes After Optimization
==========================================
{{#include ../../links.md}}
Some optimizations can alter subtle semantics of the script, causing the script to behave
differently when run with or without optimization.
Typically, this involves some form of error that may arise in the original, unoptimized script but
is optimized away by the [script optimizer][script optimization].
```admonish danger.small "DO NOT depend on runtime errors"
Needless to say, it is usually a _Very Bad Idea™_ to depend on a script failing with a runtime error
or such kind of subtleties.
If it turns out to be necessary (why? I would never guess), turn script optimization off by setting
the optimization level to [`OptimizationLevel::None`].
```
Disappearing Runtime Errors
---------------------------
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 &ndash;
the variable `hello` does not exist, so the script would have been terminated at that point
with a runtime error.
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.
Eliminated Useless Work
-----------------------
Another example is more subtle &ndash; that of an empty loop body.
```rust
// ... say, the 'Engine' is limited to no more than 10,000 operations...
// The following should fail because it exceeds the operations limit:
for n in 0..42000 {
// empty loop
}
// The above is optimized away because the loop body is empty
// and the iterations simply do nothing.
()
```
Normally, and empty loop body inside a [`for`] statement with a pure iterator does nothing and can
be safely eliminated.
Thus the script now runs silently to completion without errors.
Without optimization, the script may fail by exceeding the [maximum number of operations] allowed.

View File

@@ -1,23 +0,0 @@
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), potentially causing side-effects.
```admonish tip.small "Rule of thumb"
* _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

@@ -1,53 +0,0 @@
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 external environment and does not guarantee the same result for the same inputs.
A perfect example is a function that gets the current time &ndash; obviously each run will return a
different value!
```rust
print(get_current_time(true)); // prints the current time
// notice the call to 'get_current_time'
// has constant arguments
// The above, under full optimization level, is rewritten to:
print("10:25AM"); // the function call is replaced by
// its result at the time of optimization!
```
```admonish danger.small "Warning"
**Avoid using [`OptimizationLevel::Full`]** if volatile custom functions are involved.
```
The optimizer, when using [`OptimizationLevel::Full`], _merrily assumes_ that all functions are
_non-volatile_, 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.
~~~admonish tip "Tip: Mark a function as volatile"
All native functions are assumed to be **non-volatile**, meaning that they are eagerly called under
[`OptimizationLevel::Full`] when all arguments are [constant] (or none).
It is possible to [mark a function defined within a plugin module as volatile]({{rootUrl}}/plugins/module.md#volatile-functions)
to prevent this behavior.
```rust
#[export_module]
mod my_module {
// This function is marked 'volatile' and will not be
// eagerly executed even under OptimizationLevel::Full.
#[rhai_fn(volatile)]
pub get_current_time(am_pm: bool) -> String {
// ...
}
}
```
~~~