Script Optimization

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:


#![allow(unused)]
fn main() {
{
    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:


#![allow(unused)]
fn main() {
{
    let x = 999;
    foo(42);
    666
}
}

Constants Propagation

Constants propagation is used to remove dead code:


#![allow(unused)]
fn main() {
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.

Caveat

If the constants are modified later on (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.

This is almost never a problem because real-world scripts seldom modify a constant, but the possibility is always there.

Eager Operator Evaluations

Beware, however, that most operators are actually function calls, and those functions can be overridden, so whether they are optimized away depends on the situation:

  • If the operands are not constant values, it is not optimized.

  • If the operator is overloaded, 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 binary, it is not optimized. Only binary operators are built-in to Rhai.

  • If the operands are not of the same type, it is not optimized.

  • If the operator is not built-in (see list of built-in operators), it is not optimized.

  • If the operator is a binary built-in operator for a standard type, it is called and replaced by a constant result.

Rhai guarantees that no external function will be run (in order not to trigger side-effects) during the optimization process (unless the optimization level is set to OptimizationLevel::Full).


#![allow(unused)]
fn main() {
// 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 {
    :
}
}

Because of the eager evaluation of operators for standard types, many constant expressions will be evaluated and replaced by the result.


#![allow(unused)]
fn main() {
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:


#![allow(unused)]
fn main() {
// 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.