diff --git a/Cargo.toml b/Cargo.toml index 4ed917d9..7eb97fb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/README.md b/README.md index b5870ee1..1a346f23 100644 --- a/README.md +++ b/README.md @@ -869,13 +869,13 @@ Compound assignment operators ```rust let number = 5; -number += 4; // number = number + 4 -number -= 3; // number = number - 3 -number *= 2; // number = number * 2 -number /= 1; // number = number / 1 -number %= 3; // number = number % 3 -number <<= 2; // number = number << 2 -number >>= 1; // number = number >> 1 +number += 4; // number = number + 4 +number -= 3; // number = number - 3 +number *= 2; // number = number * 2 +number /= 1; // number = number / 1 +number %= 3; // number = number % 3 +number <<= 2; // number = number << 2 +number >>= 1; // number = number >> 1 ``` 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. Script optimization can be turned off via the [`no_optimize`] feature. @@ -1167,21 +1167,55 @@ 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 + : // this branch is eliminated } else if DECISION_3 { - : // this branch is eliminated + : // this branch is eliminated } 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. +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 diff --git a/examples/repl.rs b/examples/repl.rs index 68ae0c96..76bdcd48 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -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(); diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 2955c76e..59fe6770 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -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())); diff --git a/src/api.rs b/src/api.rs index ff57c51c..3c248d5b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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 { 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 { @@ -145,6 +149,15 @@ impl<'e> Engine<'e> { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } + /// Evaluate a file with own scope. + pub fn eval_file_with_scope( + &mut self, + scope: &mut Scope, + path: PathBuf, + ) -> Result { + Self::read_file(path).and_then(|contents| self.eval_with_scope::(scope, &contents)) + } + /// Evaluate a string. pub fn eval(&mut self, input: &str) -> Result { let mut scope = Scope::new(); @@ -180,10 +193,6 @@ impl<'e> Engine<'e> { ) -> Result { 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(()) diff --git a/src/engine.rs b/src/engine.rs index f1d9eecd..6b951d4e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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, Box>, /// A hashmap containing all script-defined functions pub(crate) script_functions: Vec>, /// A hashmap containing all iterators known to the engine pub(crate) type_iterators: HashMap>, + /// A hashmap mapping type names to pretty-print names pub(crate) type_names: HashMap, - // Closures for implementing the print/debug commands + /// Closure for implementing the print commands pub(crate) on_print: Box, + /// Closure for implementing the debug commands pub(crate) on_debug: Box, } @@ -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, 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::>(); Err(EvalAltResult::ErrorFunctionNotFound( - format!("{} ({})", spec.name, types_list.join(", ")), + format!("{} ({})", fn_name, types_list.join(", ")), pos, )) } @@ -249,7 +279,7 @@ impl Engine<'_> { .collect::, _>>()?; 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 => { diff --git a/src/lib.rs b/src/lib.rs index 47be664d..ceb34396 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/optimize.rs b/src/optimize.rs index 6013ea01..cadadd59 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -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,26 +388,47 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } } -pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { +pub(crate) fn optimize<'a>( + statements: Vec, + engine: Option<&Engine<'a>>, + scope: &Scope, +) -> Vec { + // 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; loop { - let mut state = State::new(); - let num_statements = result.len(); + state.reset(); + state.restore_constants(orig_constants_len); - 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) - }) - .for_each(|ScopeEntry { name, expr, .. }| { - state.push_constant( - name.as_ref(), - expr.as_ref().expect("should be Some(expr)").clone(), - ) - }); + let num_statements = result.len(); result = result .into_iter() @@ -405,3 +465,32 @@ pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { result } + +pub fn optimize_ast( + engine: &Engine, + scope: &Scope, + statements: Vec, + functions: Vec, +) -> 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(), + ) +} diff --git a/src/parser.rs b/src/parser.rs index e1b8d9da..d87d351b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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, - #[cfg(not(feature = "no_function"))] pub(crate) Vec>, -); +pub struct AST(pub(crate) Vec, pub(crate) Vec>); -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, @@ -267,7 +228,9 @@ pub enum Expr { FunctionCall(String, Vec, Option, Position), Assignment(Box, Box, Position), Dot(Box, Box, Position), + #[cfg(not(feature = "no_index"))] Index(Box, Box, Position), + #[cfg(not(feature = "no_index"))] Array(Vec, Position), And(Box, Box), Or(Box, Box), @@ -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::>() + .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>) -> Result (), // 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>) -> Result 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>) -> Result( +fn parse_top_level<'a, 'e>( input: &mut Peekable>, - scope: &Scope, - optimize_ast: bool, -) -> Result { +) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); - - #[cfg(not(feature = "no_function"))] let mut functions = Vec::::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>, + engine: &Engine<'e>, + scope: &Scope, +) -> Result { + 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>, - scope: &Scope, - optimize_ast: bool, -) -> Result { - parse_top_level(input, scope, optimize_ast) +pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option, Dynamic) { + if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::IntegerConstant( + *value.downcast::().expect("value should be INT"), + pos, + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::CharConstant( + *value.downcast::().expect("value should be char"), + pos, + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::StringConstant( + *value.downcast::().expect("value should be String"), + pos, + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some( + if *value.downcast::().expect("value should be bool") { + Expr::True(pos) + } else { + Expr::False(pos) + }, + ), + value2, + ) + } else { + #[cfg(not(feature = "no_float"))] + { + if value.is::() { + let value2 = value.clone(); + return ( + Some(Expr::FloatConstant( + *value.downcast::().expect("value should be FLOAT"), + pos, + )), + value2, + ); + } + } + + (None, value) + } } diff --git a/src/scope.rs b/src/scope.rs index b20eb241..92c1fe14 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -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, Dynamic) { - if value.is::() { - let value2 = value.clone(); - ( - Some(Expr::IntegerConstant( - *value.downcast::().expect("value should be INT"), - Position::none(), - )), - value2, - ) - } else if value.is::() { - let value2 = value.clone(); - ( - Some(Expr::CharConstant( - *value.downcast::().expect("value should be char"), - Position::none(), - )), - value2, - ) - } else if value.is::() { - let value2 = value.clone(); - ( - Some(Expr::StringConstant( - *value.downcast::().expect("value should be String"), - Position::none(), - )), - value2, - ) - } else if value.is::() { - let value2 = value.clone(); - ( - Some( - if *value.downcast::().expect("value should be bool") { - Expr::True(Position::none()) - } else { - Expr::False(Position::none()) - }, - ), - value2, - ) - } else { - #[cfg(not(feature = "no_float"))] - { - if value.is::() { - let value2 = value.clone(); - return ( - Some(Expr::FloatConstant( - *value.downcast::().expect("value should be FLOAT"), - Position::none(), - )), - value2, - ); - } - } - - (None, value) - } -} diff --git a/tests/engine.rs b/tests/call_fn.rs similarity index 64% rename from tests/engine.rs rename to tests/call_fn.rs index cd9f7fb0..ca6303d6 100644 --- a/tests/engine.rs +++ b/tests/call_fn.rs @@ -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); diff --git a/tests/constants.rs b/tests/constants.rs index 9a93c119..16fbf7bd 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -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::("const x = 123; x")?, 123); + assert_eq!(engine.eval::("const x = 123; x")?, 123); assert!( - matches!(engine.eval::("const x = 123; x = 42;").expect_err("expects error"), + matches!(engine.eval::("const x = 123; x = 42;").expect_err("expects error"), EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x") ); #[cfg(not(feature = "no_index"))] assert!( - matches!(engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), + matches!(engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x") ); diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 861ab4c4..a3779705 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -32,13 +32,13 @@ fn test_mismatched_op_custom_type() { .eval::("60 + new_ts()") .expect_err("expects error"); - match r { - #[cfg(feature = "only_i32")] - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)" => (), + #[cfg(feature = "only_i32")] + assert!( + matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)") + ); - #[cfg(not(feature = "only_i32"))] - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)" => (), - - _ => panic!(), - } + #[cfg(not(feature = "only_i32"))] + assert!( + matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)") + ); } diff --git a/tests/optimizer.rs b/tests/optimizer.rs new file mode 100644 index 00000000..eb9f1d5c --- /dev/null +++ b/tests/optimizer.rs @@ -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::(r"if true { 42 } else { 123 }")?, 42); + assert_eq!( + engine.eval::(r"if 1 == 1 || 2 > 3 { 42 } else { 123 }")?, + 42 + ); + assert_eq!( + engine.eval::(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(()) +}