reorganize module
This commit is contained in:
@@ -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.
|
||||
~~~
|
@@ -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
|
||||
}
|
||||
```
|
@@ -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.
|
@@ -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);
|
||||
```
|
||||
~~~
|
@@ -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 – 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.
|
@@ -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`].
|
@@ -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 |
|
@@ -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`].
|
||||
```
|
@@ -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
|
||||
```
|
||||
~~~
|
@@ -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 –
|
||||
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 – 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.
|
@@ -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.
|
||||
```
|
@@ -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 – 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 {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
~~~
|
Reference in New Issue
Block a user