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 = "*" num-traits = "*"
[features] [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 = [] default = []
debug_msgs = [] # print debug messages on function registrations and calls debug_msgs = [] # print debug messages on function registrations and calls
unchecked = [] # unchecked arithmetic unchecked = [] # unchecked arithmetic

View File

@ -869,13 +869,13 @@ Compound assignment operators
```rust ```rust
let number = 5; let number = 5;
number += 4; // number = number + 4 number += 4; // number = number + 4
number -= 3; // number = number - 3 number -= 3; // number = number - 3
number *= 2; // number = number * 2 number *= 2; // number = number * 2
number /= 1; // number = number / 1 number /= 1; // number = number / 1
number %= 3; // number = number % 3 number %= 3; // number = number % 3
number <<= 2; // number = number << 2 number <<= 2; // number = number << 2
number >>= 1; // number = number >> 1 number >>= 1; // number = number >> 1
``` ```
The `+=` operator can also be used to build strings: The `+=` operator can also be used to build strings:
@ -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. 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. Script optimization can be turned off via the [`no_optimize`] feature.
@ -1167,21 +1167,55 @@ const DECISION_2 = false;
const DECISION_3 = false; const DECISION_3 = false;
if DECISION_1 { if DECISION_1 {
: // this branch is kept : // this branch is kept and promoted to the parent level
} else if DECISION_2 { } else if DECISION_2 {
: // this branch is eliminated : // this branch is eliminated
} else if DECISION_3 { } else if DECISION_3 {
: // this branch is eliminated : // this branch is eliminated
} else { } else {
: // this branch is eliminated : // this branch is eliminated
} }
``` ```
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. 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! 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: Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example:
```rust ```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. 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. 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), 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. there is a setting in `Engine` to turn off optimizations.
```rust ```rust
let engine = rhai::Engine::new(); 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 [`Engine`]: #hello-world
[`Scope`]: #initializing-and-maintaining-state [`Scope`]: #initializing-and-maintaining-state
[`Dynamic`]: #values-and-types [`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 rhai::{Engine, EvalAltResult, Scope, AST};
use std::{ use std::{
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
iter, iter,
@ -43,6 +46,10 @@ fn print_error(input: &str, err: EvalAltResult) {
fn main() { fn main() {
let mut engine = Engine::new(); let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
let mut scope = Scope::new(); let mut scope = Scope::new();
let mut input = String::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 rhai::{Engine, EvalAltResult};
use std::{env, fs::File, io::Read, iter, process::exit}; use std::{env, fs::File, io::Read, iter, process::exit};
@ -49,6 +51,9 @@ fn main() {
for filename in env::args().skip(1) { for filename in env::args().skip(1) {
let mut engine = Engine::new(); let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
let mut f = match File::open(&filename) { let mut f = match File::open(&filename) {
Err(err) => { Err(err) => {
eprintln!("Error reading script file: {}\n{}", filename, 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!("{}", padding("=", filename.len()));
eprintln!("{}", filename); eprintln!("{}", filename);
eprintln!("{}", padding("=", filename.len())); 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::parser::{lex, parse, FnDef, Position, AST};
use crate::result::EvalAltResult; use crate::result::EvalAltResult;
use crate::scope::Scope; use crate::scope::Scope;
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize_ast;
use std::{ use std::{
any::{type_name, TypeId}, any::{type_name, TypeId},
fs::File, fs::File,
@ -108,7 +112,7 @@ impl<'e> Engine<'e> {
/// The scope is useful for passing constants into the script for optimization. /// 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> { pub fn compile_with_scope(&self, scope: &Scope, input: &str) -> Result<AST, ParseError> {
let tokens_stream = lex(input); 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> { 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)) 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. /// Evaluate a string.
pub fn eval<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> { pub fn eval<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> {
let mut scope = Scope::new(); let mut scope = Scope::new();
@ -180,10 +193,6 @@ impl<'e> Engine<'e> {
) -> Result<Dynamic, EvalAltResult> { ) -> Result<Dynamic, EvalAltResult> {
engine.clear_functions(); engine.clear_functions();
#[cfg(feature = "no_function")]
let AST(statements) = ast;
#[cfg(not(feature = "no_function"))]
let statements = { let statements = {
let AST(statements, functions) = ast; let AST(statements, functions) = ast;
engine.load_script_functions(functions); engine.load_script_functions(functions);
@ -227,10 +236,25 @@ impl<'e> Engine<'e> {
/// and not cleared from run to run. /// and not cleared from run to run.
pub fn consume_file( pub fn consume_file(
&mut self, &mut self,
path: PathBuf,
retain_functions: bool, retain_functions: bool,
path: PathBuf,
) -> Result<(), EvalAltResult> { ) -> 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). /// 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_ /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run. /// 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) 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. /// 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_ /// 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> { ) -> Result<(), EvalAltResult> {
let tokens_stream = lex(input); 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)?; .map_err(EvalAltResult::ErrorParsing)?;
self.consume_ast_with_scope(scope, retain_functions, &ast) 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_ /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
/// and not cleared from run to run. /// 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( pub fn consume_ast_with_scope(
&mut self, &mut self,
scope: &mut Scope, scope: &mut Scope,
@ -276,10 +309,6 @@ impl<'e> Engine<'e> {
self.clear_functions(); self.clear_functions();
} }
#[cfg(feature = "no_function")]
let AST(statements) = ast;
#[cfg(not(feature = "no_function"))]
let statements = { let statements = {
let AST(ref statements, ref functions) = ast; let AST(ref statements, ref functions) = ast;
self.load_script_functions(functions); self.load_script_functions(functions);
@ -327,7 +356,7 @@ impl<'e> Engine<'e> {
/// ///
/// let mut engine = Engine::new(); /// 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))?; /// 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!`) /// Override default action of `print` (print to stdout using `println!`)
/// ///
/// # Example /// # Example
@ -379,7 +429,7 @@ impl<'e> Engine<'e> {
/// ///
/// // Override action of 'print' function /// // Override action of 'print' function
/// engine.on_print(|s| result.push_str(s)); /// engine.on_print(|s| result.push_str(s));
/// engine.consume("print(40 + 2);", false)?; /// engine.consume(false, "print(40 + 2);")?;
/// } /// }
/// assert_eq!(result, "42"); /// assert_eq!(result, "42");
/// # Ok(()) /// # Ok(())
@ -403,7 +453,7 @@ impl<'e> Engine<'e> {
/// ///
/// // Override action of 'debug' function /// // Override action of 'debug' function
/// engine.on_debug(|s| result.push_str(s)); /// engine.on_debug(|s| result.push_str(s));
/// engine.consume(r#"debug("hello");"#, false)?; /// engine.consume(false, r#"debug("hello");"#)?;
/// } /// }
/// assert_eq!(result, "\"hello\""); /// assert_eq!(result, "\"hello\"");
/// # Ok(()) /// # Ok(())

View File

@ -5,6 +5,9 @@ use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
use crate::result::EvalAltResult; use crate::result::EvalAltResult;
use crate::scope::{Scope, VariableType}; use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::OptimizationLevel;
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
use crate::INT; use crate::INT;
@ -63,17 +66,20 @@ pub struct FnSpec<'a> {
/// ``` /// ```
pub struct Engine<'e> { pub struct Engine<'e> {
/// Optimize the AST after compilation /// 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 /// A hashmap containing all compiled functions known to the engine
pub(crate) ext_functions: HashMap<FnSpec<'e>, Box<FnAny>>, pub(crate) ext_functions: HashMap<FnSpec<'e>, Box<FnAny>>,
/// A hashmap containing all script-defined functions /// A hashmap containing all script-defined functions
pub(crate) script_functions: Vec<Arc<FnDef>>, pub(crate) script_functions: Vec<Arc<FnDef>>,
/// A hashmap containing all iterators known to the engine /// A hashmap containing all iterators known to the engine
pub(crate) type_iterators: HashMap<TypeId, Box<IteratorFn>>, pub(crate) type_iterators: HashMap<TypeId, Box<IteratorFn>>,
/// A hashmap mapping type names to pretty-print names
pub(crate) type_names: HashMap<String, String>, 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>, pub(crate) on_print: Box<dyn FnMut(&str) + 'e>,
/// Closure for implementing the debug commands
pub(crate) on_debug: Box<dyn FnMut(&str) + 'e>, pub(crate) on_debug: Box<dyn FnMut(&str) + 'e>,
} }
@ -93,7 +99,8 @@ impl Engine<'_> {
// Create the new scripting Engine // Create the new scripting Engine
let mut engine = Engine { let mut engine = Engine {
optimize: true, #[cfg(not(feature = "no_optimize"))]
optimization_level: OptimizationLevel::Full,
ext_functions: HashMap::new(), ext_functions: HashMap::new(),
script_functions: Vec::new(), script_functions: Vec::new(),
type_iterators: HashMap::new(), type_iterators: HashMap::new(),
@ -110,9 +117,32 @@ impl Engine<'_> {
engine engine
} }
/// Control whether the `Engine` will optimize an AST after compilation /// Control whether and how the `Engine` will optimize an AST after compilation
pub fn set_optimization(&mut self, optimize: bool) { #[cfg(not(feature = "no_optimize"))]
self.optimize = 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 /// 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()), 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) { if let Some(func) = self.ext_functions.get(&spec) {
// Run external function // Run external function
let result = func(args, pos)?; let result = func(args, pos)?;
// See if the function match print/debug (which requires special processing) // 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_PRINT => self.on_print.as_mut(),
KEYWORD_DEBUG => self.on_debug.as_mut(), KEYWORD_DEBUG => self.on_debug.as_mut(),
_ => return Ok(result), _ => return Ok(result),
@ -185,7 +215,7 @@ impl Engine<'_> {
return Ok(callback(val).into_dynamic()); 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 // Handle `type_of` function
return Ok(self return Ok(self
.map_type_name(args[0].type_name()) .map_type_name(args[0].type_name())
@ -193,23 +223,23 @@ impl Engine<'_> {
.into_dynamic()); .into_dynamic());
} }
if spec.name.starts_with(FUNC_GETTER) { if fn_name.starts_with(FUNC_GETTER) {
// Getter function not found // Getter function not found
return Err(EvalAltResult::ErrorDotExpr( return Err(EvalAltResult::ErrorDotExpr(
format!( format!(
"- property '{}' unknown or write-only", "- property '{}' unknown or write-only",
&spec.name[FUNC_GETTER.len()..] &fn_name[FUNC_GETTER.len()..]
), ),
pos, pos,
)); ));
} }
if spec.name.starts_with(FUNC_SETTER) { if fn_name.starts_with(FUNC_SETTER) {
// Setter function not found // Setter function not found
return Err(EvalAltResult::ErrorDotExpr( return Err(EvalAltResult::ErrorDotExpr(
format!( format!(
"- property '{}' unknown or read-only", "- property '{}' unknown or read-only",
&spec.name[FUNC_SETTER.len()..] &fn_name[FUNC_SETTER.len()..]
), ),
pos, pos,
)); ));
@ -228,7 +258,7 @@ impl Engine<'_> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Err(EvalAltResult::ErrorFunctionNotFound( Err(EvalAltResult::ErrorFunctionNotFound(
format!("{} ({})", spec.name, types_list.join(", ")), format!("{} ({})", fn_name, types_list.join(", ")),
pos, pos,
)) ))
} }
@ -249,7 +279,7 @@ impl Engine<'_> {
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let args = once(this_ptr) let args = once(this_ptr)
.chain(values.iter_mut().map(|b| b.as_mut())) .chain(values.iter_mut().map(Dynamic::as_mut))
.collect(); .collect();
self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos) 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()) Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic())
} }
// All other variable types should be an error IndexSourceType::Expression => panic!("expression cannot be indexed for update"),
_ => panic!("array or string source type expected for indexing"),
} }
} }
@ -809,9 +838,6 @@ impl Engine<'_> {
.eval_index_expr(scope, lhs, idx_expr, *idx_pos) .eval_index_expr(scope, lhs, idx_expr, *idx_pos)
.map(|(_, _, _, x)| x), .map(|(_, _, _, x)| x),
#[cfg(feature = "no_index")]
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
// Statement block // Statement block
Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt), Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt),
@ -870,7 +896,7 @@ impl Engine<'_> {
// Error assignment to constant // Error assignment to constant
expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant( expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant(
expr.get_value_str(), expr.get_constant_str(),
lhs.position(), lhs.position(),
)), )),
@ -891,8 +917,6 @@ impl Engine<'_> {
Ok(Box::new(arr)) Ok(Box::new(arr))
} }
#[cfg(feature = "no_index")]
Expr::Array(_, _) => panic!("encountered an array during no_index!"),
// Dump AST // Dump AST
Expr::FunctionCall(fn_name, args_expr_list, _, pos) if fn_name == KEYWORD_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"))] #[cfg(not(feature = "no_float"))]
pub use parser::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"))] #![cfg(not(feature = "no_optimize"))]
use crate::engine::KEYWORD_DUMP_AST; use crate::any::Dynamic;
use crate::parser::{Expr, Stmt}; 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}; use crate::scope::{Scope, ScopeEntry, VariableType};
struct State { use std::sync::Arc;
changed: bool,
constants: Vec<(String, Expr)>, /// 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 { pub fn new() -> Self {
State { State {
changed: false, changed: false,
constants: vec![], constants: vec![],
engine: None,
} }
} }
pub fn reset(&mut self) {
self.changed = false;
}
pub fn set_dirty(&mut self) { pub fn set_dirty(&mut self) {
self.changed = true; 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 { match stmt {
Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => { Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => {
state.set_dirty(); state.set_dirty();
@ -114,7 +134,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
Stmt::Block(statements, pos) => { Stmt::Block(statements, pos) => {
let orig_len = statements.len(); let orig_len = statements.len();
let orig_constants = state.constants.len(); let orig_constants_len = state.constants.len();
let mut result: Vec<_> = statements let mut result: Vec<_> = statements
.into_iter() // For each statement .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.set_dirty();
} }
state.restore_constants(orig_constants); state.restore_constants(orig_constants_len);
match result[..] { match result[..] {
// No statements in block - change to No-op // 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 { match expr {
Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) { Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) {
Stmt::Noop(_) => { Stmt::Noop(_) => {
@ -261,8 +281,6 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
pos, pos,
), ),
}, },
#[cfg(feature = "no_index")]
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Array(items, pos) => { Expr::Array(items, pos) => {
@ -280,9 +298,6 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
Expr::Array(items, pos) 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::And(lhs, rhs) => match (*lhs, *rhs) {
(Expr::True(_), rhs) => { (Expr::True(_), rhs) => {
state.set_dirty(); 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) if id == KEYWORD_DUMP_AST => {
Expr::FunctionCall(id, args, def_value, pos) 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) => { Expr::FunctionCall(id, args, def_value, pos) => {
let orig_len = args.len(); let orig_len = args.len();
@ -341,7 +380,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
// Replace constant with value // Replace constant with value
state state
.find_constant(name) .find_constant(name)
.expect("can't find constant in scope!") .expect("should find constant in scope!")
.clone() .clone()
} }
@ -349,26 +388,47 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
} }
} }
pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> { 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;
}
// Set up the state
let mut state = State::new();
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(Expr::is_constant).unwrap_or(false)
})
.for_each(|ScopeEntry { name, expr, .. }| {
state.push_constant(
name.as_ref(),
expr.as_ref().expect("should be Some(expr)").clone(),
)
});
let orig_constants_len = state.constants.len();
// Optimization loop
let mut result = statements; let mut result = statements;
loop { loop {
let mut state = State::new(); state.reset();
let num_statements = result.len(); state.restore_constants(orig_constants_len);
scope let num_statements = result.len();
.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)
})
.for_each(|ScopeEntry { name, expr, .. }| {
state.push_constant(
name.as_ref(),
expr.as_ref().expect("should be Some(expr)").clone(),
)
});
result = result result = result
.into_iter() .into_iter()
@ -405,3 +465,32 @@ pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
result 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. //! 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::error::{LexError, ParseError, ParseErrorType};
use crate::scope::{Scope, VariableType}; use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize; use crate::optimize::optimize_ast;
use std::{ use std::{
borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc, 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. /// Compiled AST (abstract syntax tree) of a Rhai script.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AST( pub struct AST(pub(crate) Vec<Stmt>, pub(crate) Vec<Arc<FnDef>>);
pub(crate) Vec<Stmt>,
#[cfg(not(feature = "no_function"))] pub(crate) Vec<Arc<FnDef>>,
);
impl AST { #[derive(Debug, Clone)]
/// 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
pub struct FnDef { pub struct FnDef {
pub name: String, pub name: String,
pub params: Vec<String>, pub params: Vec<String>,
@ -267,7 +228,9 @@ pub enum Expr {
FunctionCall(String, Vec<Expr>, Option<Dynamic>, Position), FunctionCall(String, Vec<Expr>, Option<Dynamic>, Position),
Assignment(Box<Expr>, Box<Expr>, Position), Assignment(Box<Expr>, Box<Expr>, Position),
Dot(Box<Expr>, Box<Expr>, Position), Dot(Box<Expr>, Box<Expr>, Position),
#[cfg(not(feature = "no_index"))]
Index(Box<Expr>, Box<Expr>, Position), Index(Box<Expr>, Box<Expr>, Position),
#[cfg(not(feature = "no_index"))]
Array(Vec<Expr>, Position), Array(Vec<Expr>, Position),
And(Box<Expr>, Box<Expr>), And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>), Or(Box<Expr>, Box<Expr>),
@ -277,7 +240,30 @@ pub enum Expr {
} }
impl 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 { match self {
Expr::IntegerConstant(i, _) => i.to_string(), Expr::IntegerConstant(i, _) => i.to_string(),
Expr::CharConstant(c, _) => c.to_string(), Expr::CharConstant(c, _) => c.to_string(),
@ -286,10 +272,13 @@ impl Expr {
Expr::False(_) => "false".to_string(), Expr::False(_) => "false".to_string(),
Expr::Unit(_) => "()".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"))] #[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.to_string(), 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::Property(_, pos)
| Expr::Stmt(_, pos) | Expr::Stmt(_, pos)
| Expr::FunctionCall(_, _, _, pos) | Expr::FunctionCall(_, _, _, pos)
| Expr::Array(_, pos)
| Expr::True(pos) | Expr::True(pos)
| Expr::False(pos) | Expr::False(pos)
| Expr::Unit(pos) => *pos, | Expr::Unit(pos) => *pos,
Expr::Assignment(e, _, _) Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => {
| Expr::Dot(e, _, _) e.position()
| Expr::Index(e, _, _) }
| Expr::And(e, _)
| Expr::Or(e, _) => e.position(),
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, pos) => *pos, 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. /// A pure expression has no side effects.
pub fn is_pure(&self) -> bool { pub fn is_pure(&self) -> bool {
match self { match self {
#[cfg(not(feature = "no_index"))]
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure), 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(_, _)), expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)),
} }
} }
@ -338,11 +336,12 @@ impl Expr {
| Expr::False(_) | Expr::False(_)
| Expr::Unit(_) => true, | Expr::Unit(_) => true,
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
Expr::FloatConstant(_, _) => true, Expr::FloatConstant(_, _) => true,
#[cfg(not(feature = "no_index"))]
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
_ => false, _ => false,
} }
} }
@ -360,7 +359,9 @@ pub enum Token {
RightBrace, RightBrace,
LeftParen, LeftParen,
RightParen, RightParen,
#[cfg(not(feature = "no_index"))]
LeftBracket, LeftBracket,
#[cfg(not(feature = "no_index"))]
RightBracket, RightBracket,
Plus, Plus,
UnaryPlus, UnaryPlus,
@ -392,6 +393,7 @@ pub enum Token {
Or, Or,
Ampersand, Ampersand,
And, And,
#[cfg(not(feature = "no_function"))]
Fn, Fn,
Break, Break,
Return, Return,
@ -435,7 +437,9 @@ impl Token {
RightBrace => "}", RightBrace => "}",
LeftParen => "(", LeftParen => "(",
RightParen => ")", RightParen => ")",
#[cfg(not(feature = "no_index"))]
LeftBracket => "[", LeftBracket => "[",
#[cfg(not(feature = "no_index"))]
RightBracket => "]", RightBracket => "]",
Plus => "+", Plus => "+",
UnaryPlus => "+", UnaryPlus => "+",
@ -467,6 +471,7 @@ impl Token {
Or => "||", Or => "||",
Ampersand => "&", Ampersand => "&",
And => "&&", And => "&&",
#[cfg(not(feature = "no_function"))]
Fn => "fn", Fn => "fn",
Break => "break", Break => "break",
Return => "return", Return => "return",
@ -506,8 +511,6 @@ impl Token {
// RightBrace | {expr} - expr not unary & is closing // RightBrace | {expr} - expr not unary & is closing
LeftParen | // {-expr} - is unary LeftParen | // {-expr} - is unary
// RightParen | (expr) - expr not unary & is closing // RightParen | (expr) - expr not unary & is closing
LeftBracket | // [-expr] - is unary
// RightBracket | [expr] - expr not unary & is closing
Plus | Plus |
UnaryPlus | UnaryPlus |
Minus | Minus |
@ -551,6 +554,10 @@ impl Token {
In | In |
PowerOfAssign => true, PowerOfAssign => true,
#[cfg(not(feature = "no_index"))]
LeftBracket => true, // [-expr] - is unary
// RightBracket | [expr] - expr not unary & is closing
_ => false, _ => false,
} }
} }
@ -560,9 +567,12 @@ impl Token {
use self::Token::*; use self::Token::*;
match *self { match *self {
RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan
| Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo
| EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true, | Pipe | Or | Ampersand | And | PowerOf => true,
#[cfg(not(feature = "no_index"))]
RightBrace | RightBracket => true,
_ => false, _ => false,
} }
@ -887,9 +897,12 @@ impl<'a> TokenIterator<'a> {
"break" => Token::Break, "break" => Token::Break,
"return" => Token::Return, "return" => Token::Return,
"throw" => Token::Throw, "throw" => Token::Throw,
"fn" => Token::Fn,
"for" => Token::For, "for" => Token::For,
"in" => Token::In, "in" => Token::In,
#[cfg(not(feature = "no_function"))]
"fn" => Token::Fn,
_ => Token::Identifier(out), _ => Token::Identifier(out),
}, },
pos, pos,
@ -924,8 +937,12 @@ impl<'a> TokenIterator<'a> {
'}' => return Some((Token::RightBrace, pos)), '}' => return Some((Token::RightBrace, pos)),
'(' => return Some((Token::LeftParen, pos)), '(' => return Some((Token::LeftParen, pos)),
')' => return Some((Token::RightParen, pos)), ')' => return Some((Token::RightParen, pos)),
#[cfg(not(feature = "no_index"))]
'[' => return Some((Token::LeftBracket, pos)), '[' => return Some((Token::LeftBracket, pos)),
#[cfg(not(feature = "no_index"))]
']' => return Some((Token::RightBracket, pos)), ']' => return Some((Token::RightBracket, pos)),
'+' => { '+' => {
return Some(( return Some((
match self.char_stream.peek() { match self.char_stream.peek() {
@ -1745,6 +1762,7 @@ fn parse_binary_op<'a>(
Box::new(change_var_to_property(*rhs)), Box::new(change_var_to_property(*rhs)),
pos, pos,
), ),
#[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx, pos) => { Expr::Index(lhs, idx, pos) => {
Expr::Index(Box::new(change_var_to_property(*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() { match input.peek() {
Some(&(Token::RightBrace, _)) => (), // empty block Some(&(Token::RightBrace, _)) => (), // empty block
#[cfg(not(feature = "no_function"))]
Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)), 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 { let return_type = match token {
Token::Return => ReturnType::Return, Token::Return => ReturnType::Return,
Token::Throw => ReturnType::Exception, Token::Throw => ReturnType::Exception,
_ => panic!("unexpected token!"), _ => panic!("token should be return or throw"),
}; };
input.next(); 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>>, input: &mut Peekable<TokenIterator<'a>>,
scope: &Scope, ) -> Result<(Vec<Stmt>, Vec<FnDef>), ParseError> {
optimize_ast: bool,
) -> Result<AST, ParseError> {
let mut statements = Vec::<Stmt>::new(); let mut statements = Vec::<Stmt>::new();
#[cfg(not(feature = "no_function"))]
let mut functions = Vec::<FnDef>::new(); let mut functions = Vec::<FnDef>::new();
while input.peek().is_some() { 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( Ok(
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
AST( optimize_ast(engine, scope, statements, functions),
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(),
),
#[cfg(feature = "no_optimize")] #[cfg(feature = "no_optimize")]
AST( AST(statements, functions.into_iter().map(Arc::new).collect()),
statements,
#[cfg(not(feature = "no_function"))]
functions.into_iter().map(Arc::new).collect(),
),
) )
} }
pub fn parse<'a>( pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option<Expr>, Dynamic) {
input: &mut Peekable<TokenIterator<'a>>, if value.is::<INT>() {
scope: &Scope, let value2 = value.clone();
optimize_ast: bool, (
) -> Result<AST, ParseError> { Some(Expr::IntegerConstant(
parse_top_level(input, scope, optimize_ast) *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. //! Module that defines the `Scope` type representing a function call-stack scope.
use crate::any::{Any, AnyExt, Dynamic}; use crate::any::{Any, Dynamic};
use crate::parser::{Expr, Position, INT}; use crate::parser::{map_dynamic_to_expr, Expr, Position};
#[cfg(not(feature = "no_float"))]
use crate::parser::FLOAT;
use std::borrow::Cow; use std::borrow::Cow;
@ -73,7 +70,7 @@ impl<'a> Scope<'a> {
let value = value.into_dynamic(); let value = value.into_dynamic();
// Map into constant expressions // 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 { self.0.push(ScopeEntry {
name: name.into(), name: name.into(),
@ -93,7 +90,7 @@ impl<'a> Scope<'a> {
let value = value.into_dynamic(); let value = value.into_dynamic();
// Map into constant expressions // 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 { self.0.push(ScopeEntry {
name: name.into(), name: name.into(),
@ -110,13 +107,13 @@ impl<'a> Scope<'a> {
var_type: VariableType, var_type: VariableType,
value: Dynamic, value: Dynamic,
) { ) {
//let (expr, value) = map_dynamic_to_expr(value); let (expr, value) = map_dynamic_to_expr(value, Position::none());
self.0.push(ScopeEntry { self.0.push(ScopeEntry {
name: name.into(), name: name.into(),
var_type, var_type,
value, 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"))] #![cfg(not(feature = "no_function"))]
use rhai::{Engine, EvalAltResult, INT}; use rhai::{Engine, EvalAltResult, INT};
#[test] #[test]
fn test_engine_call_fn() -> Result<(), EvalAltResult> { fn test_call_fn() -> Result<(), EvalAltResult> {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.consume( engine.consume(
true,
r" r"
fn hello(x, y) { fn hello(x, y) {
x.len() + y x + y
} }
fn hello(x) { fn hello(x) {
x * 2 x * 2
} }
", ",
true,
)?; )?;
let r: i64 = engine.call_fn("hello", (String::from("abc"), 123 as INT))?; let r: i64 = engine.call_fn("hello", (42 as INT, 123 as INT))?;
assert_eq!(r, 126); assert_eq!(r, 165);
let r: i64 = engine.call_fn("hello", 123 as INT)?; let r: i64 = engine.call_fn("hello", 123 as INT)?;
assert_eq!(r, 246); assert_eq!(r, 246);

View File

@ -1,19 +1,19 @@
use rhai::{Engine, EvalAltResult}; use rhai::{Engine, EvalAltResult, INT};
#[test] #[test]
fn test_constant() -> Result<(), EvalAltResult> { fn test_constant() -> Result<(), EvalAltResult> {
let mut engine = Engine::new(); 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!( 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") EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
); );
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
assert!( 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") EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
); );

View File

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

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(())
}