Eagerly evaluate built-in operators for OptimizationLevel::Simple.
This commit is contained in:
parent
b91a073596
commit
0d0affd5e9
@ -16,7 +16,9 @@ Breaking changes
|
|||||||
New features
|
New features
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* `is_def_var()` to detect if variable is defined and `is_def_fn()` to detect if script function is defined.
|
* `OptimizationLevel::Simple` now eagerly evaluates built-in binary operators of primary types (if not overloaded).
|
||||||
|
* Added `is_def_var()` to detect if variable is defined and `is_def_fn()` to detect if script function is defined.
|
||||||
|
* Added `Module::get_script_fn` to get a scripted function in a module, if any, based on name and number of parameters.
|
||||||
|
|
||||||
|
|
||||||
Version 0.19.0
|
Version 0.19.0
|
||||||
|
@ -3,8 +3,8 @@ Eager Function Evaluation When Using Full Optimization Level
|
|||||||
|
|
||||||
{{#include ../../links.md}}
|
{{#include ../../links.md}}
|
||||||
|
|
||||||
When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_
|
When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_
|
||||||
evaluated all function calls with constant arguments, using the result to replace the call.
|
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).
|
This also applies to all operators (which are implemented as functions).
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ For instance, the same example above:
|
|||||||
// When compiling the following with OptimizationLevel::Full...
|
// When compiling the following with OptimizationLevel::Full...
|
||||||
|
|
||||||
const DECISION = 1;
|
const DECISION = 1;
|
||||||
// this condition is now eliminated because 'DECISION == 1'
|
// this condition is now eliminated because 'sign(DECISION) > 0'
|
||||||
if DECISION == 1 { // is a function call to the '==' function, and it returns 'true'
|
if DECISION.sign() > 0 { // is a call to the 'sign' and '>' functions, and they return 'true'
|
||||||
print("hello!"); // this block is promoted to the parent level
|
print("hello!"); // this block is promoted to the parent level
|
||||||
} else {
|
} else {
|
||||||
print("boo!"); // this block is eliminated because it is never reached
|
print("boo!"); // this block is eliminated because it is never reached
|
||||||
@ -24,13 +24,3 @@ if DECISION == 1 { // is a function call to the '==' function, and it r
|
|||||||
print("hello!"); // <- the above is equivalent to this
|
print("hello!"); // <- the above is equivalent to this
|
||||||
// ('print' and 'debug' are handled specially)
|
// ('print' and 'debug' are handled specially)
|
||||||
```
|
```
|
||||||
|
|
||||||
Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result.
|
|
||||||
This does not happen with [`OptimizationLevel::Simple`] which doesn't assume all functions to be _pure_.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// When compiling the following with OptimizationLevel::Full...
|
|
||||||
|
|
||||||
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'
|
|
||||||
```
|
|
||||||
|
@ -61,17 +61,63 @@ For fixed script texts, the constant values can be provided in a user-defined [`
|
|||||||
to the [`Engine`] for use in compilation and evaluation.
|
to the [`Engine`] for use in compilation and evaluation.
|
||||||
|
|
||||||
|
|
||||||
Watch Out for Function Calls
|
Eager Operator Evaluations
|
||||||
---------------------------
|
-------------------------
|
||||||
|
|
||||||
Beware, however, that most operators are actually function calls, and those functions can be overridden,
|
Beware, however, that most operators are actually function calls, and those functions can be overridden,
|
||||||
so they are not optimized away:
|
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][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 _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][standard types], 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`]).
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
const DECISION = 1;
|
const DECISION = 1; // this is an integer, one of the standard types
|
||||||
|
|
||||||
if DECISION == 1 { // NOT optimized away because you can define
|
if DECISION == 1 { // this is optimized into 'true'
|
||||||
: // your own '==' function to override the built-in default!
|
:
|
||||||
|
} 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.
|
||||||
|
|
||||||
|
```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 == 2 { // same here, NOT optimized away
|
||||||
:
|
:
|
||||||
@ -82,28 +128,4 @@ if DECISION == 1 { // NOT optimized away because you can define
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
because no operator functions will be run (in order not to trigger side-effects) during the optimization process
|
|
||||||
(unless the optimization level is set to [`OptimizationLevel::Full`]).
|
|
||||||
|
|
||||||
So, instead, do this:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
const DECISION_1 = true;
|
|
||||||
const DECISION_2 = false;
|
|
||||||
const DECISION_3 = false;
|
|
||||||
|
|
||||||
if DECISION_1 {
|
|
||||||
: // this branch is kept and promoted to the parent level
|
|
||||||
} else if DECISION_2 {
|
|
||||||
: // this branch is eliminated
|
|
||||||
} else if DECISION_3 {
|
|
||||||
: // this branch is eliminated
|
|
||||||
} else {
|
|
||||||
: // this branch is eliminated
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In general, boolean constants are most effective for the optimizer to automatically prune
|
|
||||||
large `if`-`else` branches because they do not depend on operators.
|
|
||||||
|
|
||||||
Alternatively, turn the optimizer to [`OptimizationLevel::Full`].
|
Alternatively, turn the optimizer to [`OptimizationLevel::Full`].
|
||||||
|
@ -8,9 +8,10 @@ There are three levels of optimization: `None`, `Simple` and `Full`.
|
|||||||
* `None` is obvious - no optimization on the AST is performed.
|
* `None` is obvious - no optimization on the AST is performed.
|
||||||
|
|
||||||
* `Simple` (default) performs only relatively _safe_ optimizations without causing side-effects
|
* `Simple` (default) performs only relatively _safe_ optimizations without causing side-effects
|
||||||
(i.e. it only relies on static analysis and will not actually perform any function calls).
|
(i.e. it only relies on static analysis and [built-in operators] for constant [standard types],
|
||||||
|
and will not perform any external function calls).
|
||||||
|
|
||||||
* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
|
* `Full` is _much_ more aggressive, _including_ calling external functions on constant arguments to determine their result.
|
||||||
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
|
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,11 +16,11 @@ The final, optimized [`AST`] is then used for evaluations.
|
|||||||
// Compile master script to AST
|
// Compile master script to AST
|
||||||
let master_ast = engine.compile(
|
let master_ast = engine.compile(
|
||||||
r"
|
r"
|
||||||
if SCENARIO_1 {
|
if SCENARIO == 1 {
|
||||||
do_work();
|
do_work();
|
||||||
} else if SCENARIO_2 {
|
} else if SCENARIO == 2 {
|
||||||
do_something();
|
do_something();
|
||||||
} else if SCENARIO_3 {
|
} else if SCENARIO == 3 {
|
||||||
do_something_else();
|
do_something_else();
|
||||||
} else {
|
} else {
|
||||||
do_nothing();
|
do_nothing();
|
||||||
@ -29,9 +29,7 @@ r"
|
|||||||
|
|
||||||
// Create a new 'Scope' - put constants in it to aid optimization
|
// Create a new 'Scope' - put constants in it to aid optimization
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
scope.push_constant("SCENARIO_1", true);
|
scope.push_constant("SCENARIO", 1_i64);
|
||||||
scope.push_constant("SCENARIO_2", false);
|
|
||||||
scope.push_constant("SCENARIO_3", false);
|
|
||||||
|
|
||||||
// Re-optimize the AST
|
// Re-optimize the AST
|
||||||
let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple);
|
let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple);
|
||||||
|
@ -3,17 +3,20 @@ Side-Effect Considerations for Full Optimization Level
|
|||||||
|
|
||||||
{{#include ../../links.md}}
|
{{#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
|
All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_
|
||||||
nor cause any side-effects, with the exception of `print` and `debug` which are handled specially) so using
|
(i.e. they do not mutate state nor cause any side-effects, with the exception of `print` and `debug`
|
||||||
[`OptimizationLevel::Full`] is usually quite safe _unless_ custom types and functions are registered.
|
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, 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
|
If custom functions are registered to overload built-in operators, they will also be called when
|
||||||
(in an `if` statement, for example) causing side-effects.
|
the operators are used (in an `if` statement, for example) causing side-effects.
|
||||||
|
|
||||||
Therefore, the rule-of-thumb is:
|
Therefore, the rule-of-thumb is:
|
||||||
|
|
||||||
* _Always_ register custom types and functions _after_ compiling scripts if [`OptimizationLevel::Full`] is used.
|
* _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.
|
* _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.
|
||||||
|
@ -100,6 +100,7 @@
|
|||||||
[function namespaces]: {{rootUrl}}/language/fn-namespaces.md
|
[function namespaces]: {{rootUrl}}/language/fn-namespaces.md
|
||||||
[anonymous function]: {{rootUrl}}/language/fn-anon.md
|
[anonymous function]: {{rootUrl}}/language/fn-anon.md
|
||||||
[anonymous functions]: {{rootUrl}}/language/fn-anon.md
|
[anonymous functions]: {{rootUrl}}/language/fn-anon.md
|
||||||
|
[operator overloading]: {{rootUrl}}/rust/operators.md
|
||||||
|
|
||||||
[`Module`]: {{rootUrl}}/language/modules/index.md
|
[`Module`]: {{rootUrl}}/language/modules/index.md
|
||||||
[module]: {{rootUrl}}/language/modules/index.md
|
[module]: {{rootUrl}}/language/modules/index.md
|
||||||
|
@ -146,7 +146,7 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(not(feature = "no_optimize"))]
|
#[cfg(not(feature = "no_optimize"))]
|
||||||
{
|
{
|
||||||
ast = engine.optimize_ast(&scope, r, OptimizationLevel::Full);
|
ast = engine.optimize_ast(&scope, r, OptimizationLevel::Simple);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "no_optimize")]
|
#[cfg(feature = "no_optimize")]
|
||||||
|
@ -439,7 +439,33 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Has a system function an override?
|
// Has a system function an override?
|
||||||
fn has_override(&self, lib: &Module, hash_fn: u64, hash_script: u64, pub_only: bool) -> bool {
|
pub(crate) fn has_override_by_name_and_arguments(
|
||||||
|
&self,
|
||||||
|
lib: &Module,
|
||||||
|
name: &str,
|
||||||
|
arg_types: &[TypeId],
|
||||||
|
pub_only: bool,
|
||||||
|
) -> bool {
|
||||||
|
let arg_len = if arg_types.is_empty() {
|
||||||
|
usize::MAX
|
||||||
|
} else {
|
||||||
|
arg_types.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
let hash_fn = calc_fn_hash(empty(), name, arg_len, arg_types.iter().cloned());
|
||||||
|
let hash_script = calc_fn_hash(empty(), name, arg_types.len(), empty());
|
||||||
|
|
||||||
|
self.has_override(lib, hash_fn, hash_script, pub_only)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has a system function an override?
|
||||||
|
pub(crate) fn has_override(
|
||||||
|
&self,
|
||||||
|
lib: &Module,
|
||||||
|
hash_fn: u64,
|
||||||
|
hash_script: u64,
|
||||||
|
pub_only: bool,
|
||||||
|
) -> bool {
|
||||||
// NOTE: We skip script functions for global_module and packages, and native functions for lib
|
// NOTE: We skip script functions for global_module and packages, and native functions for lib
|
||||||
|
|
||||||
// First check script-defined functions
|
// First check script-defined functions
|
||||||
|
@ -294,6 +294,23 @@ impl Module {
|
|||||||
hash_script
|
hash_script
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a script-defined function in the module based on name and number of parameters.
|
||||||
|
pub fn get_script_fn(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
num_params: usize,
|
||||||
|
public_only: bool,
|
||||||
|
) -> Option<&Shared<ScriptFnDef>> {
|
||||||
|
self.functions
|
||||||
|
.values()
|
||||||
|
.find(|(fn_name, access, num, _, _)| {
|
||||||
|
(!public_only || *access == FnAccess::Public)
|
||||||
|
&& *num == num_params
|
||||||
|
&& fn_name == name
|
||||||
|
})
|
||||||
|
.map(|(_, _, _, _, f)| f.get_shared_fn_def())
|
||||||
|
}
|
||||||
|
|
||||||
/// Does a sub-module exist in the module?
|
/// Does a sub-module exist in the module?
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -5,6 +5,7 @@ use crate::calc_fn_hash;
|
|||||||
use crate::engine::{
|
use crate::engine::{
|
||||||
Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF,
|
Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF,
|
||||||
};
|
};
|
||||||
|
use crate::fn_call::run_builtin_binary_op;
|
||||||
use crate::fn_native::FnPtr;
|
use crate::fn_native::FnPtr;
|
||||||
use crate::module::Module;
|
use crate::module::Module;
|
||||||
use crate::parser::{map_dynamic_to_expr, Expr, ScriptFnDef, Stmt, AST};
|
use crate::parser::{map_dynamic_to_expr, Expr, ScriptFnDef, Stmt, AST};
|
||||||
@ -568,28 +569,46 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call built-in functions
|
||||||
|
Expr::FnCall(mut x)
|
||||||
|
if x.1.is_none() // Non-qualified
|
||||||
|
&& state.optimization_level == OptimizationLevel::Simple // simple optimizations
|
||||||
|
&& x.3.len() == 2 // binary call
|
||||||
|
&& x.3.iter().all(Expr::is_constant) // all arguments are constants
|
||||||
|
=> {
|
||||||
|
let ((name, _, _, pos), _, _, args, _) = x.as_mut();
|
||||||
|
|
||||||
|
let arg_values: StaticVec<_> = args.iter().map(Expr::get_constant_value).collect();
|
||||||
|
let arg_types: StaticVec<_> = arg_values.iter().map(Dynamic::type_id).collect();
|
||||||
|
|
||||||
|
// Search for overloaded operators (can override built-in).
|
||||||
|
if !state.engine.has_override_by_name_and_arguments(state.lib, name, arg_types.as_ref(), false) {
|
||||||
|
if let Some(expr) = run_builtin_binary_op(name, &arg_values[0], &arg_values[1])
|
||||||
|
.ok().flatten()
|
||||||
|
.and_then(|result| map_dynamic_to_expr(result, *pos))
|
||||||
|
{
|
||||||
|
state.set_dirty();
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect();
|
||||||
|
Expr::FnCall(x)
|
||||||
|
}
|
||||||
|
|
||||||
// Eagerly call functions
|
// Eagerly call functions
|
||||||
Expr::FnCall(mut x)
|
Expr::FnCall(mut x)
|
||||||
if x.1.is_none() // Non-qualified
|
if x.1.is_none() // Non-qualified
|
||||||
&& state.optimization_level == OptimizationLevel::Full // full optimizations
|
&& state.optimization_level == OptimizationLevel::Full // full optimizations
|
||||||
&& x.3.iter().all(|expr| expr.is_constant()) // all arguments are constants
|
&& x.3.iter().all(Expr::is_constant) // all arguments are constants
|
||||||
=> {
|
=> {
|
||||||
let ((name, _, _, pos), _, _, args, def_value) = x.as_mut();
|
let ((name, _, _, pos), _, _, args, def_value) = x.as_mut();
|
||||||
|
|
||||||
// First search in functions lib (can override built-in)
|
// First search for script-defined functions (can override built-in)
|
||||||
// Cater for both normal function call style and method call style (one additional arguments)
|
let has_script_fn = cfg!(not(feature = "no_function"))
|
||||||
let has_script_fn = cfg!(not(feature = "no_function")) && state.lib.iter_fn().find(|(_, _, _, _,f)| {
|
&& state.lib.get_script_fn(name, args.len(), false).is_some();
|
||||||
if !f.is_script() { return false; }
|
|
||||||
let fn_def = f.get_fn_def();
|
|
||||||
fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len())
|
|
||||||
}).is_some();
|
|
||||||
|
|
||||||
if has_script_fn {
|
|
||||||
// A script-defined function overrides the built-in function - do not make the call
|
|
||||||
x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect();
|
|
||||||
return Expr::FnCall(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if !has_script_fn {
|
||||||
let mut arg_values: StaticVec<_> = args.iter().map(Expr::get_constant_value).collect();
|
let mut arg_values: StaticVec<_> = args.iter().map(Expr::get_constant_value).collect();
|
||||||
|
|
||||||
// Save the typename of the first argument if it is `type_of()`
|
// Save the typename of the first argument if it is `type_of()`
|
||||||
@ -600,7 +619,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
|
|||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
call_fn_with_constant_arguments(&state, name, arg_values.as_mut())
|
if let Some(expr) = call_fn_with_constant_arguments(&state, name, arg_values.as_mut())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
if !arg_for_type_of.is_empty() {
|
if !arg_for_type_of.is_empty() {
|
||||||
// Handle `type_of()`
|
// Handle `type_of()`
|
||||||
@ -611,15 +630,14 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.and_then(|result| map_dynamic_to_expr(result, *pos))
|
.and_then(|result| map_dynamic_to_expr(result, *pos))
|
||||||
.map(|expr| {
|
{
|
||||||
state.set_dirty();
|
state.set_dirty();
|
||||||
expr
|
return expr;
|
||||||
})
|
}
|
||||||
.unwrap_or_else(|| {
|
}
|
||||||
// Optimize function call arguments
|
|
||||||
x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect();
|
x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect();
|
||||||
Expr::FnCall(x)
|
Expr::FnCall(x)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// id(args ..) -> optimize function call arguments
|
// id(args ..) -> optimize function call arguments
|
||||||
|
Loading…
Reference in New Issue
Block a user