Constants Propagation
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 constant expressions.
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.