Constants Propagation


Effective in template-based machine-generated scripts to turn on/off certain sections.

Constants propagation is commonly used to:

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

[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!");
    }
"#)?;
```

[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!");
    }
"#)?;
```

[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
")?;
```

[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].

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.