Add full optimization level for aggressive optimizing.

This commit is contained in:
Stephen Chung 2020-03-15 22:39:58 +08:00
parent f80e499e84
commit 372321dfe3
14 changed files with 512 additions and 272 deletions

View File

@ -18,7 +18,7 @@ include = [
num-traits = "*"
[features]
#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked"]
#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"]
default = []
debug_msgs = [] # print debug messages on function registrations and calls
unchecked = [] # unchecked arithmetic

View File

@ -1096,8 +1096,8 @@ for entry in log {
}
```
Optimizations
=============
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.
@ -1167,7 +1167,7 @@ const DECISION_2 = false;
const DECISION_3 = false;
if DECISION_1 {
: // this branch is kept
: // this branch is kept and promoted to the parent level
} else if DECISION_2 {
: // this branch is eliminated
} else if DECISION_3 {
@ -1179,9 +1179,43 @@ if DECISION_1 {
In general, boolean constants are most effective if you want the optimizer to automatically prune large `if`-`else` branches because they do not depend on operators.
Alternatively, turn the optimizer to [`OptimizationLevel::Full`]
Here be dragons!
----------------
### Optimization levels
There are actually three levels of optimizations: `None`, `Simple` and `Full`.
`None` is obvious - no optimization on the AST is performed.
`Simple` performs relatively _safe_ optimizations without causing side effects (i.e. it only relies on static analysis and will not actually perform any function calls). `Simple` is the default.
`Full` is _much_ more aggressive, _including_ running 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.
```rust
// The following run with OptimizationLevel::Full
const DECISION = 1;
if DECISION == 1 { // this condition is now eliminated because 'DECISION == 1' is a function call to the '==' function, and it returns 'true'
print("hello!"); // the 'true' block is promoted to the parent level
} else {
print("boo!"); // the 'else' block is eliminated
}
print("hello!"); // <- the above is equivalent to this
```
### Side effect considerations
All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe.
Beware, however, that if custom functions are registered, they'll also be called. If custom functions are registered to replace built-in operator functions,
the custom functions will be called and _may_ cause side-effects.
### Subtle semantic changes
Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example:
```rust
@ -1214,12 +1248,14 @@ 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, thus the script silently runs to completion without errors.
### Turning off optimizations
It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess),
there is a setting in `Engine` to turn off optimizations.
```rust
let engine = rhai::Engine::new();
engine.set_optimization(false); // turn off the optimizer
engine.set_optimization_level(rhai::OptimizationLevel::None); // turn off the optimizer
```
@ -1237,3 +1273,5 @@ engine.set_optimization(false); // turn off the optimizer
[`Engine`]: #hello-world
[`Scope`]: #initializing-and-maintaining-state
[`Dynamic`]: #values-and-types
[`OptimizationLevel::Full`]: #optimization-levels

View File

@ -1,4 +1,7 @@
#[cfg(not(feature = "no_optimize"))]
use rhai::OptimizationLevel;
use rhai::{Engine, EvalAltResult, Scope, AST};
use std::{
io::{stdin, stdout, Write},
iter,
@ -43,6 +46,10 @@ fn print_error(input: &str, err: EvalAltResult) {
fn main() {
let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
let mut scope = Scope::new();
let mut input = String::new();

View File

@ -1,3 +1,5 @@
#[cfg(not(feature = "no_optimize"))]
use rhai::OptimizationLevel;
use rhai::{Engine, EvalAltResult};
use std::{env, fs::File, io::Read, iter, process::exit};
@ -49,6 +51,9 @@ fn main() {
for filename in env::args().skip(1) {
let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
let mut f = match File::open(&filename) {
Err(err) => {
eprintln!("Error reading script file: {}\n{}", filename, err);
@ -67,7 +72,7 @@ fn main() {
_ => (),
}
if let Err(err) = engine.consume(&contents, false) {
if let Err(err) = engine.consume(false, &contents) {
eprintln!("{}", padding("=", filename.len()));
eprintln!("{}", filename);
eprintln!("{}", padding("=", filename.len()));

View File

@ -8,6 +8,10 @@ use crate::fn_register::RegisterFn;
use crate::parser::{lex, parse, FnDef, Position, AST};
use crate::result::EvalAltResult;
use crate::scope::Scope;
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize_ast;
use std::{
any::{type_name, TypeId},
fs::File,
@ -108,7 +112,7 @@ impl<'e> Engine<'e> {
/// The scope is useful for passing constants into the script for optimization.
pub fn compile_with_scope(&self, scope: &Scope, input: &str) -> Result<AST, ParseError> {
let tokens_stream = lex(input);
parse(&mut tokens_stream.peekable(), scope, self.optimize)
parse(&mut tokens_stream.peekable(), self, scope)
}
fn read_file(path: PathBuf) -> Result<String, EvalAltResult> {
@ -145,6 +149,15 @@ impl<'e> Engine<'e> {
Self::read_file(path).and_then(|contents| self.eval::<T>(&contents))
}
/// Evaluate a file with own scope.
pub fn eval_file_with_scope<T: Any + Clone>(
&mut self,
scope: &mut Scope,
path: PathBuf,
) -> Result<T, EvalAltResult> {
Self::read_file(path).and_then(|contents| self.eval_with_scope::<T>(scope, &contents))
}
/// Evaluate a string.
pub fn eval<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> {
let mut scope = Scope::new();
@ -180,10 +193,6 @@ impl<'e> Engine<'e> {
) -> Result<Dynamic, EvalAltResult> {
engine.clear_functions();
#[cfg(feature = "no_function")]
let AST(statements) = ast;
#[cfg(not(feature = "no_function"))]
let statements = {
let AST(statements, functions) = ast;
engine.load_script_functions(functions);
@ -227,10 +236,25 @@ impl<'e> Engine<'e> {
/// and not cleared from run to run.
pub fn consume_file(
&mut self,
path: PathBuf,
retain_functions: bool,
path: PathBuf,
) -> Result<(), EvalAltResult> {
Self::read_file(path).and_then(|contents| self.consume(&contents, retain_functions))
Self::read_file(path).and_then(|contents| self.consume(retain_functions, &contents))
}
/// Evaluate a file with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume_file_with_scope(
&mut self,
scope: &mut Scope,
retain_functions: bool,
path: PathBuf,
) -> Result<(), EvalAltResult> {
Self::read_file(path)
.and_then(|contents| self.consume_with_scope(scope, retain_functions, &contents))
}
/// Evaluate a string, but throw away the result and only return error (if any).
@ -238,11 +262,11 @@ impl<'e> Engine<'e> {
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume(&mut self, input: &str, retain_functions: bool) -> Result<(), EvalAltResult> {
pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> {
self.consume_with_scope(&mut Scope::new(), retain_functions, input)
}
/// Evaluate a string, but throw away the result and only return error (if any).
/// Evaluate a string with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
@ -255,7 +279,7 @@ impl<'e> Engine<'e> {
) -> Result<(), EvalAltResult> {
let tokens_stream = lex(input);
let ast = parse(&mut tokens_stream.peekable(), scope, self.optimize)
let ast = parse(&mut tokens_stream.peekable(), self, scope)
.map_err(EvalAltResult::ErrorParsing)?;
self.consume_ast_with_scope(scope, retain_functions, &ast)
@ -266,6 +290,15 @@ impl<'e> Engine<'e> {
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> {
self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast)
}
/// Evaluate an AST with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
///
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run.
pub fn consume_ast_with_scope(
&mut self,
scope: &mut Scope,
@ -276,10 +309,6 @@ impl<'e> Engine<'e> {
self.clear_functions();
}
#[cfg(feature = "no_function")]
let AST(statements) = ast;
#[cfg(not(feature = "no_function"))]
let statements = {
let AST(ref statements, ref functions) = ast;
self.load_script_functions(functions);
@ -327,7 +356,7 @@ impl<'e> Engine<'e> {
///
/// let mut engine = Engine::new();
///
/// engine.consume("fn add(x, y) { x.len() + y }", true)?;
/// engine.consume(true, "fn add(x, y) { x.len() + y }")?;
///
/// let result: i64 = engine.call_fn("add", (String::from("abc"), 123_i64))?;
///
@ -365,6 +394,27 @@ impl<'e> Engine<'e> {
})
}
/// Optimize the AST with constants defined in an external Scope.
/// An optimized copy of the AST is returned while the original AST is untouched.
///
/// Although optimization is performed by default during compilation, sometimes it is necessary to
/// _re_-optimize an AST. For example, when working with constants that are passed in via an
/// external scope, it will be more efficient to optimize the AST once again to take advantage
/// of the new constants.
///
/// With this method, it is no longer necessary to recompile a large script. The script AST can be
/// compiled just once. Before evaluation, constants are passed into the `Engine` via an external scope
/// (i.e. with `scope.push_constant(...)`). Then, the AST is cloned and the copy re-optimized before running.
#[cfg(not(feature = "no_optimize"))]
pub fn optimize_ast(&self, scope: &Scope, ast: &AST) -> AST {
optimize_ast(
self,
scope,
ast.0.clone(),
ast.1.iter().map(|f| (**f).clone()).collect(),
)
}
/// Override default action of `print` (print to stdout using `println!`)
///
/// # Example
@ -379,7 +429,7 @@ impl<'e> Engine<'e> {
///
/// // Override action of 'print' function
/// engine.on_print(|s| result.push_str(s));
/// engine.consume("print(40 + 2);", false)?;
/// engine.consume(false, "print(40 + 2);")?;
/// }
/// assert_eq!(result, "42");
/// # Ok(())
@ -403,7 +453,7 @@ impl<'e> Engine<'e> {
///
/// // Override action of 'debug' function
/// engine.on_debug(|s| result.push_str(s));
/// engine.consume(r#"debug("hello");"#, false)?;
/// engine.consume(false, r#"debug("hello");"#)?;
/// }
/// assert_eq!(result, "\"hello\"");
/// # Ok(())

View File

@ -5,6 +5,9 @@ use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
use crate::result::EvalAltResult;
use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::OptimizationLevel;
#[cfg(not(feature = "no_index"))]
use crate::INT;
@ -63,17 +66,20 @@ pub struct FnSpec<'a> {
/// ```
pub struct Engine<'e> {
/// Optimize the AST after compilation
pub(crate) optimize: bool,
#[cfg(not(feature = "no_optimize"))]
pub(crate) optimization_level: OptimizationLevel,
/// A hashmap containing all compiled functions known to the engine
pub(crate) ext_functions: HashMap<FnSpec<'e>, Box<FnAny>>,
/// A hashmap containing all script-defined functions
pub(crate) script_functions: Vec<Arc<FnDef>>,
/// A hashmap containing all iterators known to the engine
pub(crate) type_iterators: HashMap<TypeId, Box<IteratorFn>>,
/// A hashmap mapping type names to pretty-print names
pub(crate) type_names: HashMap<String, String>,
// Closures for implementing the print/debug commands
/// Closure for implementing the print commands
pub(crate) on_print: Box<dyn FnMut(&str) + 'e>,
/// Closure for implementing the debug commands
pub(crate) on_debug: Box<dyn FnMut(&str) + 'e>,
}
@ -93,7 +99,8 @@ impl Engine<'_> {
// Create the new scripting Engine
let mut engine = Engine {
optimize: true,
#[cfg(not(feature = "no_optimize"))]
optimization_level: OptimizationLevel::Full,
ext_functions: HashMap::new(),
script_functions: Vec::new(),
type_iterators: HashMap::new(),
@ -110,9 +117,32 @@ impl Engine<'_> {
engine
}
/// Control whether the `Engine` will optimize an AST after compilation
pub fn set_optimization(&mut self, optimize: bool) {
self.optimize = optimize
/// Control whether and how the `Engine` will optimize an AST after compilation
#[cfg(not(feature = "no_optimize"))]
pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) {
self.optimization_level = optimization_level
}
/// Call a registered function
#[cfg(not(feature = "no_optimize"))]
pub(crate) fn call_ext_fn_raw(
&self,
fn_name: &str,
args: FnCallArgs,
pos: Position,
) -> Result<Option<Dynamic>, EvalAltResult> {
let spec = FnSpec {
name: fn_name.into(),
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
};
// Search built-in's and external functions
if let Some(func) = self.ext_functions.get(&spec) {
// Run external function
Ok(Some(func(args, pos)?))
} else {
Ok(None)
}
}
/// Universal method for calling functions, that are either
@ -165,13 +195,13 @@ impl Engine<'_> {
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
};
// Then search built-in's and external functions
// Search built-in's and external functions
if let Some(func) = self.ext_functions.get(&spec) {
// Run external function
let result = func(args, pos)?;
// See if the function match print/debug (which requires special processing)
let callback = match spec.name.as_ref() {
let callback = match fn_name {
KEYWORD_PRINT => self.on_print.as_mut(),
KEYWORD_DEBUG => self.on_debug.as_mut(),
_ => return Ok(result),
@ -185,7 +215,7 @@ impl Engine<'_> {
return Ok(callback(val).into_dynamic());
}
if spec.name == KEYWORD_TYPE_OF && args.len() == 1 {
if fn_name == KEYWORD_TYPE_OF && args.len() == 1 {
// Handle `type_of` function
return Ok(self
.map_type_name(args[0].type_name())
@ -193,23 +223,23 @@ impl Engine<'_> {
.into_dynamic());
}
if spec.name.starts_with(FUNC_GETTER) {
if fn_name.starts_with(FUNC_GETTER) {
// Getter function not found
return Err(EvalAltResult::ErrorDotExpr(
format!(
"- property '{}' unknown or write-only",
&spec.name[FUNC_GETTER.len()..]
&fn_name[FUNC_GETTER.len()..]
),
pos,
));
}
if spec.name.starts_with(FUNC_SETTER) {
if fn_name.starts_with(FUNC_SETTER) {
// Setter function not found
return Err(EvalAltResult::ErrorDotExpr(
format!(
"- property '{}' unknown or read-only",
&spec.name[FUNC_SETTER.len()..]
&fn_name[FUNC_SETTER.len()..]
),
pos,
));
@ -228,7 +258,7 @@ impl Engine<'_> {
.collect::<Vec<_>>();
Err(EvalAltResult::ErrorFunctionNotFound(
format!("{} ({})", spec.name, types_list.join(", ")),
format!("{} ({})", fn_name, types_list.join(", ")),
pos,
))
}
@ -249,7 +279,7 @@ impl Engine<'_> {
.collect::<Result<Vec<_>, _>>()?;
let args = once(this_ptr)
.chain(values.iter_mut().map(|b| b.as_mut()))
.chain(values.iter_mut().map(Dynamic::as_mut))
.collect();
self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos)
@ -567,8 +597,7 @@ impl Engine<'_> {
Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic())
}
// All other variable types should be an error
_ => panic!("array or string source type expected for indexing"),
IndexSourceType::Expression => panic!("expression cannot be indexed for update"),
}
}
@ -809,9 +838,6 @@ impl Engine<'_> {
.eval_index_expr(scope, lhs, idx_expr, *idx_pos)
.map(|(_, _, _, x)| x),
#[cfg(feature = "no_index")]
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
// Statement block
Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt),
@ -870,7 +896,7 @@ impl Engine<'_> {
// Error assignment to constant
expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant(
expr.get_value_str(),
expr.get_constant_str(),
lhs.position(),
)),
@ -891,8 +917,6 @@ impl Engine<'_> {
Ok(Box::new(arr))
}
#[cfg(feature = "no_index")]
Expr::Array(_, _) => panic!("encountered an array during no_index!"),
// Dump AST
Expr::FunctionCall(fn_name, args_expr_list, _, pos) if fn_name == KEYWORD_DUMP_AST => {

View File

@ -87,3 +87,6 @@ pub use engine::Array;
#[cfg(not(feature = "no_float"))]
pub use parser::FLOAT;
#[cfg(not(feature = "no_optimize"))]
pub use optimize::OptimizationLevel;

View File

@ -1,21 +1,41 @@
#![cfg(not(feature = "no_optimize"))]
use crate::engine::KEYWORD_DUMP_AST;
use crate::parser::{Expr, Stmt};
use crate::any::Dynamic;
use crate::engine::{Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT};
use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST};
use crate::scope::{Scope, ScopeEntry, VariableType};
struct State {
changed: bool,
constants: Vec<(String, Expr)>,
use std::sync::Arc;
/// Level of optimization performed
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
pub enum OptimizationLevel {
/// No optimization performed
None,
/// Only perform simple optimizations without evaluating functions
Simple,
/// Full optimizations performed, including evaluating functions.
/// Take care that this may cause side effects.
Full,
}
impl State {
struct State<'a> {
changed: bool,
constants: Vec<(String, Expr)>,
engine: Option<&'a Engine<'a>>,
}
impl State<'_> {
pub fn new() -> Self {
State {
changed: false,
constants: vec![],
engine: None,
}
}
pub fn reset(&mut self) {
self.changed = false;
}
pub fn set_dirty(&mut self) {
self.changed = true;
}
@ -42,7 +62,7 @@ impl State {
}
}
fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt {
match stmt {
Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => {
state.set_dirty();
@ -114,7 +134,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
Stmt::Block(statements, pos) => {
let orig_len = statements.len();
let orig_constants = state.constants.len();
let orig_constants_len = state.constants.len();
let mut result: Vec<_> = statements
.into_iter() // For each statement
@ -175,7 +195,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
state.set_dirty();
}
state.restore_constants(orig_constants);
state.restore_constants(orig_constants_len);
match result[..] {
// No statements in block - change to No-op
@ -202,7 +222,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
}
}
fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr {
match expr {
Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) {
Stmt::Noop(_) => {
@ -261,8 +281,6 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
pos,
),
},
#[cfg(feature = "no_index")]
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
#[cfg(not(feature = "no_index"))]
Expr::Array(items, pos) => {
@ -280,9 +298,6 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
Expr::Array(items, pos)
}
#[cfg(feature = "no_index")]
Expr::Array(_, _) => panic!("encountered an array during no_index!"),
Expr::And(lhs, rhs) => match (*lhs, *rhs) {
(Expr::True(_), rhs) => {
state.set_dirty();
@ -320,9 +335,33 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
),
},
// Do not optimize anything within `dump_ast`
Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST => {
Expr::FunctionCall(id, args, def_value, pos)
}
// Actually call function to optimize it
Expr::FunctionCall(id, args, def_value, pos)
if id != KEYWORD_DEBUG // not debug
&& id != KEYWORD_PRINT // not print
&& state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations
&& args.iter().all(|expr| expr.is_constant()) // all arguments are constants
=>
{
let engine = state.engine.expect("engine should be Some");
let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect();
let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect();
if let Ok(r) = engine.call_ext_fn_raw(&id, call_args, pos) {
r.and_then(|result| map_dynamic_to_expr(result, pos).0)
.map(|expr| {
state.set_dirty();
expr
})
.unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos))
} else {
Expr::FunctionCall(id, args, def_value, pos)
}
}
// Optimize the function call arguments
Expr::FunctionCall(id, args, def_value, pos) => {
let orig_len = args.len();
@ -341,7 +380,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
// Replace constant with value
state
.find_constant(name)
.expect("can't find constant in scope!")
.expect("should find constant in scope!")
.clone()
}
@ -349,19 +388,29 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
}
}
pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
let mut result = statements;
pub(crate) fn optimize<'a>(
statements: Vec<Stmt>,
engine: Option<&Engine<'a>>,
scope: &Scope,
) -> Vec<Stmt> {
// If optimization level is None then skip optimizing
if engine
.map(|eng| eng.optimization_level == OptimizationLevel::None)
.unwrap_or(false)
{
return statements;
}
loop {
// Set up the state
let mut state = State::new();
let num_statements = result.len();
state.engine = engine;
scope
.iter()
.filter(|ScopeEntry { var_type, expr, .. }| {
// Get all the constants with definite constant expressions
*var_type == VariableType::Constant
&& expr.as_ref().map(|e| e.is_constant()).unwrap_or(false)
&& expr.as_ref().map(Expr::is_constant).unwrap_or(false)
})
.for_each(|ScopeEntry { name, expr, .. }| {
state.push_constant(
@ -370,6 +419,17 @@ pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
)
});
let orig_constants_len = state.constants.len();
// Optimization loop
let mut result = statements;
loop {
state.reset();
state.restore_constants(orig_constants_len);
let num_statements = result.len();
result = result
.into_iter()
.enumerate()
@ -405,3 +465,32 @@ pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
result
}
pub fn optimize_ast(
engine: &Engine,
scope: &Scope,
statements: Vec<Stmt>,
functions: Vec<FnDef>,
) -> AST {
AST(
match engine.optimization_level {
OptimizationLevel::None => statements,
OptimizationLevel::Simple => optimize(statements, None, &scope),
OptimizationLevel::Full => optimize(statements, Some(engine), &scope),
},
functions
.into_iter()
.map(|mut fn_def| {
match engine.optimization_level {
OptimizationLevel::None => (),
OptimizationLevel::Simple | OptimizationLevel::Full => {
let pos = fn_def.body.position();
let mut body = optimize(vec![fn_def.body], None, &Scope::new());
fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
}
}
Arc::new(fn_def)
})
.collect(),
)
}

View File

@ -1,11 +1,12 @@
//! Main module defining the lexer and parser.
use crate::any::Dynamic;
use crate::any::{Any, AnyExt, Dynamic};
use crate::engine::Engine;
use crate::error::{LexError, ParseError, ParseErrorType};
use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize;
use crate::optimize::optimize_ast;
use std::{
borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc,
@ -147,49 +148,9 @@ impl fmt::Debug for Position {
/// Compiled AST (abstract syntax tree) of a Rhai script.
#[derive(Debug, Clone)]
pub struct AST(
pub(crate) Vec<Stmt>,
#[cfg(not(feature = "no_function"))] pub(crate) Vec<Arc<FnDef>>,
);
pub struct AST(pub(crate) Vec<Stmt>, pub(crate) Vec<Arc<FnDef>>);
impl AST {
/// Optimize the AST with constants defined in an external Scope.
///
/// Although optimization is performed by default during compilation, sometimes it is necessary to
/// _re_-optimize an AST. For example, when working with constants that are passed in via an
/// external scope, it will be more efficient to optimize the AST once again to take advantage
/// of the new constants.
///
/// With this method, it is no longer necessary to regenerate a large script with hard-coded
/// constant values. The script AST can be compiled just once. During actual evaluation,
/// constants are passed into the Engine via an external scope (i.e. with `scope.push_constant(...)`).
/// Then, the AST is cloned and the copy re-optimized before running.
#[cfg(not(feature = "no_optimize"))]
pub fn optimize(self, scope: &Scope) -> Self {
AST(
crate::optimize::optimize(self.0, scope),
#[cfg(not(feature = "no_function"))]
self.1
.into_iter()
.map(|fn_def| {
let pos = fn_def.body.position();
let body = optimize(vec![fn_def.body.clone()], scope)
.into_iter()
.next()
.unwrap_or_else(|| Stmt::Noop(pos));
Arc::new(FnDef {
name: fn_def.name.clone(),
params: fn_def.params.clone(),
body,
pos: fn_def.pos,
})
})
.collect(),
)
}
}
#[derive(Debug)] // Do not derive Clone because it is expensive
#[derive(Debug, Clone)]
pub struct FnDef {
pub name: String,
pub params: Vec<String>,
@ -267,7 +228,9 @@ pub enum Expr {
FunctionCall(String, Vec<Expr>, Option<Dynamic>, Position),
Assignment(Box<Expr>, Box<Expr>, Position),
Dot(Box<Expr>, Box<Expr>, Position),
#[cfg(not(feature = "no_index"))]
Index(Box<Expr>, Box<Expr>, Position),
#[cfg(not(feature = "no_index"))]
Array(Vec<Expr>, Position),
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
@ -277,7 +240,30 @@ pub enum Expr {
}
impl Expr {
pub fn get_value_str(&self) -> String {
pub fn get_constant_value(&self) -> Dynamic {
match self {
Expr::IntegerConstant(i, _) => i.into_dynamic(),
Expr::CharConstant(c, _) => c.into_dynamic(),
Expr::StringConstant(s, _) => s.into_dynamic(),
Expr::True(_) => true.into_dynamic(),
Expr::False(_) => false.into_dynamic(),
Expr::Unit(_) => ().into_dynamic(),
#[cfg(not(feature = "no_index"))]
Expr::Array(items, _) if items.iter().all(Expr::is_constant) => items
.iter()
.map(Expr::get_constant_value)
.collect::<Vec<_>>()
.into_dynamic(),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.into_dynamic(),
_ => panic!("cannot get value of non-constant expression"),
}
}
pub fn get_constant_str(&self) -> String {
match self {
Expr::IntegerConstant(i, _) => i.to_string(),
Expr::CharConstant(c, _) => c.to_string(),
@ -286,10 +272,13 @@ impl Expr {
Expr::False(_) => "false".to_string(),
Expr::Unit(_) => "()".to_string(),
#[cfg(not(feature = "no_index"))]
Expr::Array(items, _) if items.iter().all(Expr::is_constant) => "array".to_string(),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.to_string(),
_ => "".to_string(),
_ => panic!("cannot get value of non-constant expression"),
}
}
@ -302,19 +291,22 @@ impl Expr {
| Expr::Property(_, pos)
| Expr::Stmt(_, pos)
| Expr::FunctionCall(_, _, _, pos)
| Expr::Array(_, pos)
| Expr::True(pos)
| Expr::False(pos)
| Expr::Unit(pos) => *pos,
Expr::Assignment(e, _, _)
| Expr::Dot(e, _, _)
| Expr::Index(e, _, _)
| Expr::And(e, _)
| Expr::Or(e, _) => e.position(),
Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => {
e.position()
}
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, pos) => *pos,
#[cfg(not(feature = "no_index"))]
Expr::Array(_, pos) => *pos,
#[cfg(not(feature = "no_index"))]
Expr::Index(e, _, _) => e.position(),
}
}
@ -323,8 +315,14 @@ impl Expr {
/// A pure expression has no side effects.
pub fn is_pure(&self) -> bool {
match self {
#[cfg(not(feature = "no_index"))]
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure),
Expr::And(x, y) | Expr::Or(x, y) | Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
#[cfg(not(feature = "no_index"))]
Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(),
expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)),
}
}
@ -338,11 +336,12 @@ impl Expr {
| Expr::False(_)
| Expr::Unit(_) => true,
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, _) => true,
#[cfg(not(feature = "no_index"))]
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
_ => false,
}
}
@ -360,7 +359,9 @@ pub enum Token {
RightBrace,
LeftParen,
RightParen,
#[cfg(not(feature = "no_index"))]
LeftBracket,
#[cfg(not(feature = "no_index"))]
RightBracket,
Plus,
UnaryPlus,
@ -392,6 +393,7 @@ pub enum Token {
Or,
Ampersand,
And,
#[cfg(not(feature = "no_function"))]
Fn,
Break,
Return,
@ -435,7 +437,9 @@ impl Token {
RightBrace => "}",
LeftParen => "(",
RightParen => ")",
#[cfg(not(feature = "no_index"))]
LeftBracket => "[",
#[cfg(not(feature = "no_index"))]
RightBracket => "]",
Plus => "+",
UnaryPlus => "+",
@ -467,6 +471,7 @@ impl Token {
Or => "||",
Ampersand => "&",
And => "&&",
#[cfg(not(feature = "no_function"))]
Fn => "fn",
Break => "break",
Return => "return",
@ -506,8 +511,6 @@ impl Token {
// RightBrace | {expr} - expr not unary & is closing
LeftParen | // {-expr} - is unary
// RightParen | (expr) - expr not unary & is closing
LeftBracket | // [-expr] - is unary
// RightBracket | [expr] - expr not unary & is closing
Plus |
UnaryPlus |
Minus |
@ -551,6 +554,10 @@ impl Token {
In |
PowerOfAssign => true,
#[cfg(not(feature = "no_index"))]
LeftBracket => true, // [-expr] - is unary
// RightBracket | [expr] - expr not unary & is closing
_ => false,
}
}
@ -560,9 +567,12 @@ impl Token {
use self::Token::*;
match *self {
RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma
| Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo
| EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true,
RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan
| GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo
| Pipe | Or | Ampersand | And | PowerOf => true,
#[cfg(not(feature = "no_index"))]
RightBrace | RightBracket => true,
_ => false,
}
@ -887,9 +897,12 @@ impl<'a> TokenIterator<'a> {
"break" => Token::Break,
"return" => Token::Return,
"throw" => Token::Throw,
"fn" => Token::Fn,
"for" => Token::For,
"in" => Token::In,
#[cfg(not(feature = "no_function"))]
"fn" => Token::Fn,
_ => Token::Identifier(out),
},
pos,
@ -924,8 +937,12 @@ impl<'a> TokenIterator<'a> {
'}' => return Some((Token::RightBrace, pos)),
'(' => return Some((Token::LeftParen, pos)),
')' => return Some((Token::RightParen, pos)),
#[cfg(not(feature = "no_index"))]
'[' => return Some((Token::LeftBracket, pos)),
#[cfg(not(feature = "no_index"))]
']' => return Some((Token::RightBracket, pos)),
'+' => {
return Some((
match self.char_stream.peek() {
@ -1745,6 +1762,7 @@ fn parse_binary_op<'a>(
Box::new(change_var_to_property(*rhs)),
pos,
),
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx, pos) => {
Expr::Index(Box::new(change_var_to_property(*lhs)), idx, pos)
}
@ -1950,6 +1968,8 @@ fn parse_block<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Pars
match input.peek() {
Some(&(Token::RightBrace, _)) => (), // empty block
#[cfg(not(feature = "no_function"))]
Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)),
_ => {
@ -2003,7 +2023,7 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
let return_type = match token {
Token::Return => ReturnType::Return,
Token::Throw => ReturnType::Exception,
_ => panic!("unexpected token!"),
_ => panic!("token should be return or throw"),
};
input.next();
@ -2095,14 +2115,10 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
})
}
fn parse_top_level<'a>(
fn parse_top_level<'a, 'e>(
input: &mut Peekable<TokenIterator<'a>>,
scope: &Scope,
optimize_ast: bool,
) -> Result<AST, ParseError> {
) -> Result<(Vec<Stmt>, Vec<FnDef>), ParseError> {
let mut statements = Vec::<Stmt>::new();
#[cfg(not(feature = "no_function"))]
let mut functions = Vec::<FnDef>::new();
while input.peek().is_some() {
@ -2126,40 +2142,79 @@ fn parse_top_level<'a>(
}
}
Ok((statements, functions))
}
pub fn parse<'a, 'e>(
input: &mut Peekable<TokenIterator<'a>>,
engine: &Engine<'e>,
scope: &Scope,
) -> Result<AST, ParseError> {
let (statements, functions) = parse_top_level(input)?;
Ok(
#[cfg(not(feature = "no_optimize"))]
AST(
if optimize_ast {
optimize(statements, &scope)
} else {
statements
},
#[cfg(not(feature = "no_function"))]
functions
.into_iter()
.map(|mut fn_def| {
if optimize_ast {
let pos = fn_def.body.position();
let mut body = optimize(vec![fn_def.body], &scope);
fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
}
Arc::new(fn_def)
})
.collect(),
),
optimize_ast(engine, scope, statements, functions),
#[cfg(feature = "no_optimize")]
AST(
statements,
#[cfg(not(feature = "no_function"))]
functions.into_iter().map(Arc::new).collect(),
),
AST(statements, functions.into_iter().map(Arc::new).collect()),
)
}
pub fn parse<'a>(
input: &mut Peekable<TokenIterator<'a>>,
scope: &Scope,
optimize_ast: bool,
) -> Result<AST, ParseError> {
parse_top_level(input, scope, optimize_ast)
pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option<Expr>, Dynamic) {
if value.is::<INT>() {
let value2 = value.clone();
(
Some(Expr::IntegerConstant(
*value.downcast::<INT>().expect("value should be INT"),
pos,
)),
value2,
)
} else if value.is::<char>() {
let value2 = value.clone();
(
Some(Expr::CharConstant(
*value.downcast::<char>().expect("value should be char"),
pos,
)),
value2,
)
} else if value.is::<String>() {
let value2 = value.clone();
(
Some(Expr::StringConstant(
*value.downcast::<String>().expect("value should be String"),
pos,
)),
value2,
)
} else if value.is::<bool>() {
let value2 = value.clone();
(
Some(
if *value.downcast::<bool>().expect("value should be bool") {
Expr::True(pos)
} else {
Expr::False(pos)
},
),
value2,
)
} else {
#[cfg(not(feature = "no_float"))]
{
if value.is::<FLOAT>() {
let value2 = value.clone();
return (
Some(Expr::FloatConstant(
*value.downcast::<FLOAT>().expect("value should be FLOAT"),
pos,
)),
value2,
);
}
}
(None, value)
}
}

View File

@ -1,10 +1,7 @@
//! Module that defines the `Scope` type representing a function call-stack scope.
use crate::any::{Any, AnyExt, Dynamic};
use crate::parser::{Expr, Position, INT};
#[cfg(not(feature = "no_float"))]
use crate::parser::FLOAT;
use crate::any::{Any, Dynamic};
use crate::parser::{map_dynamic_to_expr, Expr, Position};
use std::borrow::Cow;
@ -73,7 +70,7 @@ impl<'a> Scope<'a> {
let value = value.into_dynamic();
// Map into constant expressions
//let (expr, value) = map_dynamic_to_expr(value);
//let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry {
name: name.into(),
@ -93,7 +90,7 @@ impl<'a> Scope<'a> {
let value = value.into_dynamic();
// Map into constant expressions
let (expr, value) = map_dynamic_to_expr(value);
let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry {
name: name.into(),
@ -110,13 +107,13 @@ impl<'a> Scope<'a> {
var_type: VariableType,
value: Dynamic,
) {
//let (expr, value) = map_dynamic_to_expr(value);
let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry {
name: name.into(),
var_type,
value,
expr: None,
expr,
});
}
@ -210,62 +207,3 @@ where
}));
}
}
fn map_dynamic_to_expr(value: Dynamic) -> (Option<Expr>, Dynamic) {
if value.is::<INT>() {
let value2 = value.clone();
(
Some(Expr::IntegerConstant(
*value.downcast::<INT>().expect("value should be INT"),
Position::none(),
)),
value2,
)
} else if value.is::<char>() {
let value2 = value.clone();
(
Some(Expr::CharConstant(
*value.downcast::<char>().expect("value should be char"),
Position::none(),
)),
value2,
)
} else if value.is::<String>() {
let value2 = value.clone();
(
Some(Expr::StringConstant(
*value.downcast::<String>().expect("value should be String"),
Position::none(),
)),
value2,
)
} else if value.is::<bool>() {
let value2 = value.clone();
(
Some(
if *value.downcast::<bool>().expect("value should be bool") {
Expr::True(Position::none())
} else {
Expr::False(Position::none())
},
),
value2,
)
} else {
#[cfg(not(feature = "no_float"))]
{
if value.is::<FLOAT>() {
let value2 = value.clone();
return (
Some(Expr::FloatConstant(
*value.downcast::<FLOAT>().expect("value should be FLOAT"),
Position::none(),
)),
value2,
);
}
}
(None, value)
}
}

View File

@ -1,25 +1,24 @@
#![cfg(not(feature = "no_stdlib"))]
#![cfg(not(feature = "no_function"))]
use rhai::{Engine, EvalAltResult, INT};
#[test]
fn test_engine_call_fn() -> Result<(), EvalAltResult> {
fn test_call_fn() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
engine.consume(
true,
r"
fn hello(x, y) {
x.len() + y
x + y
}
fn hello(x) {
x * 2
}
",
true,
)?;
let r: i64 = engine.call_fn("hello", (String::from("abc"), 123 as INT))?;
assert_eq!(r, 126);
let r: i64 = engine.call_fn("hello", (42 as INT, 123 as INT))?;
assert_eq!(r, 165);
let r: i64 = engine.call_fn("hello", 123 as INT)?;
assert_eq!(r, 246);

View File

@ -1,19 +1,19 @@
use rhai::{Engine, EvalAltResult};
use rhai::{Engine, EvalAltResult, INT};
#[test]
fn test_constant() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(engine.eval::<i64>("const x = 123; x")?, 123);
assert_eq!(engine.eval::<INT>("const x = 123; x")?, 123);
assert!(
matches!(engine.eval::<i64>("const x = 123; x = 42;").expect_err("expects error"),
matches!(engine.eval::<INT>("const x = 123; x = 42;").expect_err("expects error"),
EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
);
#[cfg(not(feature = "no_index"))]
assert!(
matches!(engine.eval::<i64>("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"),
matches!(engine.eval::<INT>("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"),
EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
);

View File

@ -32,13 +32,13 @@ fn test_mismatched_op_custom_type() {
.eval::<INT>("60 + new_ts()")
.expect_err("expects error");
match r {
#[cfg(feature = "only_i32")]
EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)" => (),
assert!(
matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)")
);
#[cfg(not(feature = "only_i32"))]
EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)" => (),
_ => panic!(),
}
assert!(
matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)")
);
}

32
tests/optimizer.rs Normal file
View File

@ -0,0 +1,32 @@
#![cfg(not(feature = "no_optimize"))]
use rhai::{Engine, EvalAltResult, OptimizationLevel, INT};
#[test]
fn test_optimizer() -> Result<(), EvalAltResult> {
fn run_test(engine: &mut Engine) -> Result<(), EvalAltResult> {
assert_eq!(engine.eval::<INT>(r"if true { 42 } else { 123 }")?, 42);
assert_eq!(
engine.eval::<INT>(r"if 1 == 1 || 2 > 3 { 42 } else { 123 }")?,
42
);
assert_eq!(
engine.eval::<INT>(r#"const abc = "hello"; if abc < "foo" { 42 } else { 123 }"#)?,
123
);
Ok(())
}
let mut engine = Engine::new();
engine.set_optimization_level(OptimizationLevel::None);
run_test(&mut engine)?;
engine.set_optimization_level(OptimizationLevel::Simple);
run_test(&mut engine)?;
engine.set_optimization_level(OptimizationLevel::Full);
run_test(&mut engine)?;
Ok(())
}