Add full optimization level for aggressive optimizing.
This commit is contained in:
parent
f80e499e84
commit
372321dfe3
@ -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
|
||||
|
46
README.md
46
README.md
@ -1096,8 +1096,8 @@ for entry in log {
|
||||
}
|
||||
```
|
||||
|
||||
Optimizations
|
||||
=============
|
||||
Script optimization
|
||||
===================
|
||||
|
||||
Rhai includes an _optimizer_ that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed.
|
||||
Script optimization can be turned off via the [`no_optimize`] feature.
|
||||
@ -1167,7 +1167,7 @@ const DECISION_2 = false;
|
||||
const DECISION_3 = false;
|
||||
|
||||
if DECISION_1 {
|
||||
: // this branch is kept
|
||||
: // this branch is kept and promoted to the parent level
|
||||
} else if DECISION_2 {
|
||||
: // this branch is eliminated
|
||||
} else if DECISION_3 {
|
||||
@ -1179,9 +1179,43 @@ if DECISION_1 {
|
||||
|
||||
In general, boolean constants are most effective if you want the optimizer to automatically prune large `if`-`else` branches because they do not depend on operators.
|
||||
|
||||
Alternatively, turn the optimizer to [`OptimizationLevel::Full`]
|
||||
|
||||
Here be dragons!
|
||||
----------------
|
||||
|
||||
### Optimization levels
|
||||
|
||||
There are actually three levels of optimizations: `None`, `Simple` and `Full`.
|
||||
|
||||
`None` is obvious - no optimization on the AST is performed.
|
||||
|
||||
`Simple` performs relatively _safe_ optimizations without causing side effects (i.e. it only relies on static analysis and will not actually perform any function calls). `Simple` is the default.
|
||||
|
||||
`Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
|
||||
|
||||
```rust
|
||||
// The following run with OptimizationLevel::Full
|
||||
|
||||
const DECISION = 1;
|
||||
|
||||
if DECISION == 1 { // this condition is now eliminated because 'DECISION == 1' is a function call to the '==' function, and it returns 'true'
|
||||
print("hello!"); // the 'true' block is promoted to the parent level
|
||||
} else {
|
||||
print("boo!"); // the 'else' block is eliminated
|
||||
}
|
||||
|
||||
print("hello!"); // <- the above is equivalent to this
|
||||
```
|
||||
|
||||
### Side effect considerations
|
||||
|
||||
All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe.
|
||||
Beware, however, that if custom functions are registered, they'll also be called. If custom functions are registered to replace built-in operator functions,
|
||||
the custom functions will be called and _may_ cause side-effects.
|
||||
|
||||
### Subtle semantic changes
|
||||
|
||||
Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example:
|
||||
|
||||
```rust
|
||||
@ -1214,12 +1248,14 @@ print("end!");
|
||||
In the script above, if `my_decision` holds anything other than a boolean value, the script should have been terminated due to a type error.
|
||||
However, after optimization, the entire `if` statement is removed, thus the script silently runs to completion without errors.
|
||||
|
||||
### Turning off optimizations
|
||||
|
||||
It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess),
|
||||
there is a setting in `Engine` to turn off optimizations.
|
||||
|
||||
```rust
|
||||
let engine = rhai::Engine::new();
|
||||
engine.set_optimization(false); // turn off the optimizer
|
||||
engine.set_optimization_level(rhai::OptimizationLevel::None); // turn off the optimizer
|
||||
```
|
||||
|
||||
|
||||
@ -1237,3 +1273,5 @@ engine.set_optimization(false); // turn off the optimizer
|
||||
[`Engine`]: #hello-world
|
||||
[`Scope`]: #initializing-and-maintaining-state
|
||||
[`Dynamic`]: #values-and-types
|
||||
|
||||
[`OptimizationLevel::Full`]: #optimization-levels
|
||||
|
@ -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();
|
||||
|
@ -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()));
|
||||
|
84
src/api.rs
84
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<AST, ParseError> {
|
||||
let tokens_stream = lex(input);
|
||||
parse(&mut tokens_stream.peekable(), scope, self.optimize)
|
||||
parse(&mut tokens_stream.peekable(), self, scope)
|
||||
}
|
||||
|
||||
fn read_file(path: PathBuf) -> Result<String, EvalAltResult> {
|
||||
@ -145,6 +149,15 @@ impl<'e> Engine<'e> {
|
||||
Self::read_file(path).and_then(|contents| self.eval::<T>(&contents))
|
||||
}
|
||||
|
||||
/// Evaluate a file with own scope.
|
||||
pub fn eval_file_with_scope<T: Any + Clone>(
|
||||
&mut self,
|
||||
scope: &mut Scope,
|
||||
path: PathBuf,
|
||||
) -> Result<T, EvalAltResult> {
|
||||
Self::read_file(path).and_then(|contents| self.eval_with_scope::<T>(scope, &contents))
|
||||
}
|
||||
|
||||
/// Evaluate a string.
|
||||
pub fn eval<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> {
|
||||
let mut scope = Scope::new();
|
||||
@ -180,10 +193,6 @@ impl<'e> Engine<'e> {
|
||||
) -> Result<Dynamic, EvalAltResult> {
|
||||
engine.clear_functions();
|
||||
|
||||
#[cfg(feature = "no_function")]
|
||||
let AST(statements) = ast;
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
let statements = {
|
||||
let AST(statements, functions) = ast;
|
||||
engine.load_script_functions(functions);
|
||||
@ -227,10 +236,25 @@ impl<'e> Engine<'e> {
|
||||
/// and not cleared from run to run.
|
||||
pub fn consume_file(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
retain_functions: bool,
|
||||
path: PathBuf,
|
||||
) -> Result<(), EvalAltResult> {
|
||||
Self::read_file(path).and_then(|contents| self.consume(&contents, retain_functions))
|
||||
Self::read_file(path).and_then(|contents| self.consume(retain_functions, &contents))
|
||||
}
|
||||
|
||||
/// Evaluate a file with own scope, but throw away the result and only return error (if any).
|
||||
/// Useful for when you don't need the result, but still need to keep track of possible errors.
|
||||
///
|
||||
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
|
||||
/// and not cleared from run to run.
|
||||
pub fn consume_file_with_scope(
|
||||
&mut self,
|
||||
scope: &mut Scope,
|
||||
retain_functions: bool,
|
||||
path: PathBuf,
|
||||
) -> Result<(), EvalAltResult> {
|
||||
Self::read_file(path)
|
||||
.and_then(|contents| self.consume_with_scope(scope, retain_functions, &contents))
|
||||
}
|
||||
|
||||
/// Evaluate a string, but throw away the result and only return error (if any).
|
||||
@ -238,11 +262,11 @@ impl<'e> Engine<'e> {
|
||||
///
|
||||
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
|
||||
/// and not cleared from run to run.
|
||||
pub fn consume(&mut self, input: &str, retain_functions: bool) -> Result<(), EvalAltResult> {
|
||||
pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> {
|
||||
self.consume_with_scope(&mut Scope::new(), retain_functions, input)
|
||||
}
|
||||
|
||||
/// Evaluate a string, but throw away the result and only return error (if any).
|
||||
/// Evaluate a string with own scope, but throw away the result and only return error (if any).
|
||||
/// Useful for when you don't need the result, but still need to keep track of possible errors.
|
||||
///
|
||||
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
|
||||
@ -255,7 +279,7 @@ impl<'e> Engine<'e> {
|
||||
) -> Result<(), EvalAltResult> {
|
||||
let tokens_stream = lex(input);
|
||||
|
||||
let ast = parse(&mut tokens_stream.peekable(), scope, self.optimize)
|
||||
let ast = parse(&mut tokens_stream.peekable(), self, scope)
|
||||
.map_err(EvalAltResult::ErrorParsing)?;
|
||||
|
||||
self.consume_ast_with_scope(scope, retain_functions, &ast)
|
||||
@ -266,6 +290,15 @@ impl<'e> Engine<'e> {
|
||||
///
|
||||
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
|
||||
/// and not cleared from run to run.
|
||||
pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> {
|
||||
self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast)
|
||||
}
|
||||
|
||||
/// Evaluate an AST with own scope, but throw away the result and only return error (if any).
|
||||
/// Useful for when you don't need the result, but still need to keep track of possible errors.
|
||||
///
|
||||
/// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_
|
||||
/// and not cleared from run to run.
|
||||
pub fn consume_ast_with_scope(
|
||||
&mut self,
|
||||
scope: &mut Scope,
|
||||
@ -276,10 +309,6 @@ impl<'e> Engine<'e> {
|
||||
self.clear_functions();
|
||||
}
|
||||
|
||||
#[cfg(feature = "no_function")]
|
||||
let AST(statements) = ast;
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
let statements = {
|
||||
let AST(ref statements, ref functions) = ast;
|
||||
self.load_script_functions(functions);
|
||||
@ -327,7 +356,7 @@ impl<'e> Engine<'e> {
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
///
|
||||
/// engine.consume("fn add(x, y) { x.len() + y }", true)?;
|
||||
/// engine.consume(true, "fn add(x, y) { x.len() + y }")?;
|
||||
///
|
||||
/// let result: i64 = engine.call_fn("add", (String::from("abc"), 123_i64))?;
|
||||
///
|
||||
@ -365,6 +394,27 @@ impl<'e> Engine<'e> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Optimize the AST with constants defined in an external Scope.
|
||||
/// An optimized copy of the AST is returned while the original AST is untouched.
|
||||
///
|
||||
/// Although optimization is performed by default during compilation, sometimes it is necessary to
|
||||
/// _re_-optimize an AST. For example, when working with constants that are passed in via an
|
||||
/// external scope, it will be more efficient to optimize the AST once again to take advantage
|
||||
/// of the new constants.
|
||||
///
|
||||
/// With this method, it is no longer necessary to recompile a large script. The script AST can be
|
||||
/// compiled just once. Before evaluation, constants are passed into the `Engine` via an external scope
|
||||
/// (i.e. with `scope.push_constant(...)`). Then, the AST is cloned and the copy re-optimized before running.
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
pub fn optimize_ast(&self, scope: &Scope, ast: &AST) -> AST {
|
||||
optimize_ast(
|
||||
self,
|
||||
scope,
|
||||
ast.0.clone(),
|
||||
ast.1.iter().map(|f| (**f).clone()).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Override default action of `print` (print to stdout using `println!`)
|
||||
///
|
||||
/// # Example
|
||||
@ -379,7 +429,7 @@ impl<'e> Engine<'e> {
|
||||
///
|
||||
/// // Override action of 'print' function
|
||||
/// engine.on_print(|s| result.push_str(s));
|
||||
/// engine.consume("print(40 + 2);", false)?;
|
||||
/// engine.consume(false, "print(40 + 2);")?;
|
||||
/// }
|
||||
/// assert_eq!(result, "42");
|
||||
/// # Ok(())
|
||||
@ -403,7 +453,7 @@ impl<'e> Engine<'e> {
|
||||
///
|
||||
/// // Override action of 'debug' function
|
||||
/// engine.on_debug(|s| result.push_str(s));
|
||||
/// engine.consume(r#"debug("hello");"#, false)?;
|
||||
/// engine.consume(false, r#"debug("hello");"#)?;
|
||||
/// }
|
||||
/// assert_eq!(result, "\"hello\"");
|
||||
/// # Ok(())
|
||||
|
@ -5,6 +5,9 @@ use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
|
||||
use crate::result::EvalAltResult;
|
||||
use crate::scope::{Scope, VariableType};
|
||||
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
use crate::optimize::OptimizationLevel;
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
use crate::INT;
|
||||
|
||||
@ -63,17 +66,20 @@ pub struct FnSpec<'a> {
|
||||
/// ```
|
||||
pub struct Engine<'e> {
|
||||
/// Optimize the AST after compilation
|
||||
pub(crate) optimize: bool,
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
pub(crate) optimization_level: OptimizationLevel,
|
||||
/// A hashmap containing all compiled functions known to the engine
|
||||
pub(crate) ext_functions: HashMap<FnSpec<'e>, Box<FnAny>>,
|
||||
/// A hashmap containing all script-defined functions
|
||||
pub(crate) script_functions: Vec<Arc<FnDef>>,
|
||||
/// A hashmap containing all iterators known to the engine
|
||||
pub(crate) type_iterators: HashMap<TypeId, Box<IteratorFn>>,
|
||||
/// A hashmap mapping type names to pretty-print names
|
||||
pub(crate) type_names: HashMap<String, String>,
|
||||
|
||||
// Closures for implementing the print/debug commands
|
||||
/// Closure for implementing the print commands
|
||||
pub(crate) on_print: Box<dyn FnMut(&str) + 'e>,
|
||||
/// Closure for implementing the debug commands
|
||||
pub(crate) on_debug: Box<dyn FnMut(&str) + 'e>,
|
||||
}
|
||||
|
||||
@ -93,7 +99,8 @@ impl Engine<'_> {
|
||||
|
||||
// Create the new scripting Engine
|
||||
let mut engine = Engine {
|
||||
optimize: true,
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
optimization_level: OptimizationLevel::Full,
|
||||
ext_functions: HashMap::new(),
|
||||
script_functions: Vec::new(),
|
||||
type_iterators: HashMap::new(),
|
||||
@ -110,9 +117,32 @@ impl Engine<'_> {
|
||||
engine
|
||||
}
|
||||
|
||||
/// Control whether the `Engine` will optimize an AST after compilation
|
||||
pub fn set_optimization(&mut self, optimize: bool) {
|
||||
self.optimize = optimize
|
||||
/// Control whether and how the `Engine` will optimize an AST after compilation
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) {
|
||||
self.optimization_level = optimization_level
|
||||
}
|
||||
|
||||
/// Call a registered function
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
pub(crate) fn call_ext_fn_raw(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
args: FnCallArgs,
|
||||
pos: Position,
|
||||
) -> Result<Option<Dynamic>, EvalAltResult> {
|
||||
let spec = FnSpec {
|
||||
name: fn_name.into(),
|
||||
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
|
||||
};
|
||||
|
||||
// Search built-in's and external functions
|
||||
if let Some(func) = self.ext_functions.get(&spec) {
|
||||
// Run external function
|
||||
Ok(Some(func(args, pos)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Universal method for calling functions, that are either
|
||||
@ -165,13 +195,13 @@ impl Engine<'_> {
|
||||
args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()),
|
||||
};
|
||||
|
||||
// Then search built-in's and external functions
|
||||
// Search built-in's and external functions
|
||||
if let Some(func) = self.ext_functions.get(&spec) {
|
||||
// Run external function
|
||||
let result = func(args, pos)?;
|
||||
|
||||
// See if the function match print/debug (which requires special processing)
|
||||
let callback = match spec.name.as_ref() {
|
||||
let callback = match fn_name {
|
||||
KEYWORD_PRINT => self.on_print.as_mut(),
|
||||
KEYWORD_DEBUG => self.on_debug.as_mut(),
|
||||
_ => return Ok(result),
|
||||
@ -185,7 +215,7 @@ impl Engine<'_> {
|
||||
return Ok(callback(val).into_dynamic());
|
||||
}
|
||||
|
||||
if spec.name == KEYWORD_TYPE_OF && args.len() == 1 {
|
||||
if fn_name == KEYWORD_TYPE_OF && args.len() == 1 {
|
||||
// Handle `type_of` function
|
||||
return Ok(self
|
||||
.map_type_name(args[0].type_name())
|
||||
@ -193,23 +223,23 @@ impl Engine<'_> {
|
||||
.into_dynamic());
|
||||
}
|
||||
|
||||
if spec.name.starts_with(FUNC_GETTER) {
|
||||
if fn_name.starts_with(FUNC_GETTER) {
|
||||
// Getter function not found
|
||||
return Err(EvalAltResult::ErrorDotExpr(
|
||||
format!(
|
||||
"- property '{}' unknown or write-only",
|
||||
&spec.name[FUNC_GETTER.len()..]
|
||||
&fn_name[FUNC_GETTER.len()..]
|
||||
),
|
||||
pos,
|
||||
));
|
||||
}
|
||||
|
||||
if spec.name.starts_with(FUNC_SETTER) {
|
||||
if fn_name.starts_with(FUNC_SETTER) {
|
||||
// Setter function not found
|
||||
return Err(EvalAltResult::ErrorDotExpr(
|
||||
format!(
|
||||
"- property '{}' unknown or read-only",
|
||||
&spec.name[FUNC_SETTER.len()..]
|
||||
&fn_name[FUNC_SETTER.len()..]
|
||||
),
|
||||
pos,
|
||||
));
|
||||
@ -228,7 +258,7 @@ impl Engine<'_> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Err(EvalAltResult::ErrorFunctionNotFound(
|
||||
format!("{} ({})", spec.name, types_list.join(", ")),
|
||||
format!("{} ({})", fn_name, types_list.join(", ")),
|
||||
pos,
|
||||
))
|
||||
}
|
||||
@ -249,7 +279,7 @@ impl Engine<'_> {
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let args = once(this_ptr)
|
||||
.chain(values.iter_mut().map(|b| b.as_mut()))
|
||||
.chain(values.iter_mut().map(Dynamic::as_mut))
|
||||
.collect();
|
||||
|
||||
self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos)
|
||||
@ -567,8 +597,7 @@ impl Engine<'_> {
|
||||
Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic())
|
||||
}
|
||||
|
||||
// All other variable types should be an error
|
||||
_ => panic!("array or string source type expected for indexing"),
|
||||
IndexSourceType::Expression => panic!("expression cannot be indexed for update"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -809,9 +838,6 @@ impl Engine<'_> {
|
||||
.eval_index_expr(scope, lhs, idx_expr, *idx_pos)
|
||||
.map(|(_, _, _, x)| x),
|
||||
|
||||
#[cfg(feature = "no_index")]
|
||||
Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"),
|
||||
|
||||
// Statement block
|
||||
Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt),
|
||||
|
||||
@ -870,7 +896,7 @@ impl Engine<'_> {
|
||||
|
||||
// Error assignment to constant
|
||||
expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant(
|
||||
expr.get_value_str(),
|
||||
expr.get_constant_str(),
|
||||
lhs.position(),
|
||||
)),
|
||||
|
||||
@ -891,8 +917,6 @@ impl Engine<'_> {
|
||||
|
||||
Ok(Box::new(arr))
|
||||
}
|
||||
#[cfg(feature = "no_index")]
|
||||
Expr::Array(_, _) => panic!("encountered an array during no_index!"),
|
||||
|
||||
// Dump AST
|
||||
Expr::FunctionCall(fn_name, args_expr_list, _, pos) if fn_name == KEYWORD_DUMP_AST => {
|
||||
|
@ -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;
|
||||
|
131
src/optimize.rs
131
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,19 +388,29 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
|
||||
let mut result = statements;
|
||||
pub(crate) fn optimize<'a>(
|
||||
statements: Vec<Stmt>,
|
||||
engine: Option<&Engine<'a>>,
|
||||
scope: &Scope,
|
||||
) -> Vec<Stmt> {
|
||||
// If optimization level is None then skip optimizing
|
||||
if engine
|
||||
.map(|eng| eng.optimization_level == OptimizationLevel::None)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return statements;
|
||||
}
|
||||
|
||||
loop {
|
||||
// Set up the state
|
||||
let mut state = State::new();
|
||||
let num_statements = result.len();
|
||||
state.engine = engine;
|
||||
|
||||
scope
|
||||
.iter()
|
||||
.filter(|ScopeEntry { var_type, expr, .. }| {
|
||||
// Get all the constants with definite constant expressions
|
||||
*var_type == VariableType::Constant
|
||||
&& expr.as_ref().map(|e| e.is_constant()).unwrap_or(false)
|
||||
&& expr.as_ref().map(Expr::is_constant).unwrap_or(false)
|
||||
})
|
||||
.for_each(|ScopeEntry { name, expr, .. }| {
|
||||
state.push_constant(
|
||||
@ -370,6 +419,17 @@ pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
|
||||
)
|
||||
});
|
||||
|
||||
let orig_constants_len = state.constants.len();
|
||||
|
||||
// Optimization loop
|
||||
let mut result = statements;
|
||||
|
||||
loop {
|
||||
state.reset();
|
||||
state.restore_constants(orig_constants_len);
|
||||
|
||||
let num_statements = result.len();
|
||||
|
||||
result = result
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@ -405,3 +465,32 @@ pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn optimize_ast(
|
||||
engine: &Engine,
|
||||
scope: &Scope,
|
||||
statements: Vec<Stmt>,
|
||||
functions: Vec<FnDef>,
|
||||
) -> AST {
|
||||
AST(
|
||||
match engine.optimization_level {
|
||||
OptimizationLevel::None => statements,
|
||||
OptimizationLevel::Simple => optimize(statements, None, &scope),
|
||||
OptimizationLevel::Full => optimize(statements, Some(engine), &scope),
|
||||
},
|
||||
functions
|
||||
.into_iter()
|
||||
.map(|mut fn_def| {
|
||||
match engine.optimization_level {
|
||||
OptimizationLevel::None => (),
|
||||
OptimizationLevel::Simple | OptimizationLevel::Full => {
|
||||
let pos = fn_def.body.position();
|
||||
let mut body = optimize(vec![fn_def.body], None, &Scope::new());
|
||||
fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
|
||||
}
|
||||
}
|
||||
Arc::new(fn_def)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
251
src/parser.rs
251
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<Stmt>,
|
||||
#[cfg(not(feature = "no_function"))] pub(crate) Vec<Arc<FnDef>>,
|
||||
);
|
||||
pub struct AST(pub(crate) Vec<Stmt>, pub(crate) Vec<Arc<FnDef>>);
|
||||
|
||||
impl AST {
|
||||
/// Optimize the AST with constants defined in an external Scope.
|
||||
///
|
||||
/// Although optimization is performed by default during compilation, sometimes it is necessary to
|
||||
/// _re_-optimize an AST. For example, when working with constants that are passed in via an
|
||||
/// external scope, it will be more efficient to optimize the AST once again to take advantage
|
||||
/// of the new constants.
|
||||
///
|
||||
/// With this method, it is no longer necessary to regenerate a large script with hard-coded
|
||||
/// constant values. The script AST can be compiled just once. During actual evaluation,
|
||||
/// constants are passed into the Engine via an external scope (i.e. with `scope.push_constant(...)`).
|
||||
/// Then, the AST is cloned and the copy re-optimized before running.
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
pub fn optimize(self, scope: &Scope) -> Self {
|
||||
AST(
|
||||
crate::optimize::optimize(self.0, scope),
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
self.1
|
||||
.into_iter()
|
||||
.map(|fn_def| {
|
||||
let pos = fn_def.body.position();
|
||||
let body = optimize(vec![fn_def.body.clone()], scope)
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| Stmt::Noop(pos));
|
||||
Arc::new(FnDef {
|
||||
name: fn_def.name.clone(),
|
||||
params: fn_def.params.clone(),
|
||||
body,
|
||||
pos: fn_def.pos,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)] // Do not derive Clone because it is expensive
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FnDef {
|
||||
pub name: String,
|
||||
pub params: Vec<String>,
|
||||
@ -267,7 +228,9 @@ pub enum Expr {
|
||||
FunctionCall(String, Vec<Expr>, Option<Dynamic>, Position),
|
||||
Assignment(Box<Expr>, Box<Expr>, Position),
|
||||
Dot(Box<Expr>, Box<Expr>, Position),
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Index(Box<Expr>, Box<Expr>, Position),
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Array(Vec<Expr>, Position),
|
||||
And(Box<Expr>, Box<Expr>),
|
||||
Or(Box<Expr>, Box<Expr>),
|
||||
@ -277,7 +240,30 @@ pub enum Expr {
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
pub fn get_value_str(&self) -> String {
|
||||
pub fn get_constant_value(&self) -> Dynamic {
|
||||
match self {
|
||||
Expr::IntegerConstant(i, _) => i.into_dynamic(),
|
||||
Expr::CharConstant(c, _) => c.into_dynamic(),
|
||||
Expr::StringConstant(s, _) => s.into_dynamic(),
|
||||
Expr::True(_) => true.into_dynamic(),
|
||||
Expr::False(_) => false.into_dynamic(),
|
||||
Expr::Unit(_) => ().into_dynamic(),
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Array(items, _) if items.iter().all(Expr::is_constant) => items
|
||||
.iter()
|
||||
.map(Expr::get_constant_value)
|
||||
.collect::<Vec<_>>()
|
||||
.into_dynamic(),
|
||||
|
||||
#[cfg(not(feature = "no_float"))]
|
||||
Expr::FloatConstant(f, _) => f.into_dynamic(),
|
||||
|
||||
_ => panic!("cannot get value of non-constant expression"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_constant_str(&self) -> String {
|
||||
match self {
|
||||
Expr::IntegerConstant(i, _) => i.to_string(),
|
||||
Expr::CharConstant(c, _) => c.to_string(),
|
||||
@ -286,10 +272,13 @@ impl Expr {
|
||||
Expr::False(_) => "false".to_string(),
|
||||
Expr::Unit(_) => "()".to_string(),
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Array(items, _) if items.iter().all(Expr::is_constant) => "array".to_string(),
|
||||
|
||||
#[cfg(not(feature = "no_float"))]
|
||||
Expr::FloatConstant(f, _) => f.to_string(),
|
||||
|
||||
_ => "".to_string(),
|
||||
_ => panic!("cannot get value of non-constant expression"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,19 +291,22 @@ impl Expr {
|
||||
| Expr::Property(_, pos)
|
||||
| Expr::Stmt(_, pos)
|
||||
| Expr::FunctionCall(_, _, _, pos)
|
||||
| Expr::Array(_, pos)
|
||||
| Expr::True(pos)
|
||||
| Expr::False(pos)
|
||||
| Expr::Unit(pos) => *pos,
|
||||
|
||||
Expr::Assignment(e, _, _)
|
||||
| Expr::Dot(e, _, _)
|
||||
| Expr::Index(e, _, _)
|
||||
| Expr::And(e, _)
|
||||
| Expr::Or(e, _) => e.position(),
|
||||
Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => {
|
||||
e.position()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "no_float"))]
|
||||
Expr::FloatConstant(_, pos) => *pos,
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Array(_, pos) => *pos,
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Index(e, _, _) => e.position(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,8 +315,14 @@ impl Expr {
|
||||
/// A pure expression has no side effects.
|
||||
pub fn is_pure(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure),
|
||||
Expr::And(x, y) | Expr::Or(x, y) | Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
|
||||
|
||||
Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(),
|
||||
|
||||
expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)),
|
||||
}
|
||||
}
|
||||
@ -338,11 +336,12 @@ impl Expr {
|
||||
| Expr::False(_)
|
||||
| Expr::Unit(_) => true,
|
||||
|
||||
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
|
||||
|
||||
#[cfg(not(feature = "no_float"))]
|
||||
Expr::FloatConstant(_, _) => true,
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant),
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -360,7 +359,9 @@ pub enum Token {
|
||||
RightBrace,
|
||||
LeftParen,
|
||||
RightParen,
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
LeftBracket,
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
RightBracket,
|
||||
Plus,
|
||||
UnaryPlus,
|
||||
@ -392,6 +393,7 @@ pub enum Token {
|
||||
Or,
|
||||
Ampersand,
|
||||
And,
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
Fn,
|
||||
Break,
|
||||
Return,
|
||||
@ -435,7 +437,9 @@ impl Token {
|
||||
RightBrace => "}",
|
||||
LeftParen => "(",
|
||||
RightParen => ")",
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
LeftBracket => "[",
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
RightBracket => "]",
|
||||
Plus => "+",
|
||||
UnaryPlus => "+",
|
||||
@ -467,6 +471,7 @@ impl Token {
|
||||
Or => "||",
|
||||
Ampersand => "&",
|
||||
And => "&&",
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
Fn => "fn",
|
||||
Break => "break",
|
||||
Return => "return",
|
||||
@ -506,8 +511,6 @@ impl Token {
|
||||
// RightBrace | {expr} - expr not unary & is closing
|
||||
LeftParen | // {-expr} - is unary
|
||||
// RightParen | (expr) - expr not unary & is closing
|
||||
LeftBracket | // [-expr] - is unary
|
||||
// RightBracket | [expr] - expr not unary & is closing
|
||||
Plus |
|
||||
UnaryPlus |
|
||||
Minus |
|
||||
@ -551,6 +554,10 @@ impl Token {
|
||||
In |
|
||||
PowerOfAssign => true,
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
LeftBracket => true, // [-expr] - is unary
|
||||
// RightBracket | [expr] - expr not unary & is closing
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -560,9 +567,12 @@ impl Token {
|
||||
use self::Token::*;
|
||||
|
||||
match *self {
|
||||
RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma
|
||||
| Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo
|
||||
| EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true,
|
||||
RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan
|
||||
| GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo
|
||||
| Pipe | Or | Ampersand | And | PowerOf => true,
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
RightBrace | RightBracket => true,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
@ -887,9 +897,12 @@ impl<'a> TokenIterator<'a> {
|
||||
"break" => Token::Break,
|
||||
"return" => Token::Return,
|
||||
"throw" => Token::Throw,
|
||||
"fn" => Token::Fn,
|
||||
"for" => Token::For,
|
||||
"in" => Token::In,
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
"fn" => Token::Fn,
|
||||
|
||||
_ => Token::Identifier(out),
|
||||
},
|
||||
pos,
|
||||
@ -924,8 +937,12 @@ impl<'a> TokenIterator<'a> {
|
||||
'}' => return Some((Token::RightBrace, pos)),
|
||||
'(' => return Some((Token::LeftParen, pos)),
|
||||
')' => return Some((Token::RightParen, pos)),
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
'[' => return Some((Token::LeftBracket, pos)),
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
']' => return Some((Token::RightBracket, pos)),
|
||||
|
||||
'+' => {
|
||||
return Some((
|
||||
match self.char_stream.peek() {
|
||||
@ -1745,6 +1762,7 @@ fn parse_binary_op<'a>(
|
||||
Box::new(change_var_to_property(*rhs)),
|
||||
pos,
|
||||
),
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Index(lhs, idx, pos) => {
|
||||
Expr::Index(Box::new(change_var_to_property(*lhs)), idx, pos)
|
||||
}
|
||||
@ -1950,6 +1968,8 @@ fn parse_block<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Pars
|
||||
|
||||
match input.peek() {
|
||||
Some(&(Token::RightBrace, _)) => (), // empty block
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)),
|
||||
|
||||
_ => {
|
||||
@ -2003,7 +2023,7 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
|
||||
let return_type = match token {
|
||||
Token::Return => ReturnType::Return,
|
||||
Token::Throw => ReturnType::Exception,
|
||||
_ => panic!("unexpected token!"),
|
||||
_ => panic!("token should be return or throw"),
|
||||
};
|
||||
|
||||
input.next();
|
||||
@ -2095,14 +2115,10 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_top_level<'a>(
|
||||
fn parse_top_level<'a, 'e>(
|
||||
input: &mut Peekable<TokenIterator<'a>>,
|
||||
scope: &Scope,
|
||||
optimize_ast: bool,
|
||||
) -> Result<AST, ParseError> {
|
||||
) -> Result<(Vec<Stmt>, Vec<FnDef>), ParseError> {
|
||||
let mut statements = Vec::<Stmt>::new();
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
let mut functions = Vec::<FnDef>::new();
|
||||
|
||||
while input.peek().is_some() {
|
||||
@ -2126,40 +2142,79 @@ fn parse_top_level<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((statements, functions))
|
||||
}
|
||||
|
||||
pub fn parse<'a, 'e>(
|
||||
input: &mut Peekable<TokenIterator<'a>>,
|
||||
engine: &Engine<'e>,
|
||||
scope: &Scope,
|
||||
) -> Result<AST, ParseError> {
|
||||
let (statements, functions) = parse_top_level(input)?;
|
||||
|
||||
Ok(
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
AST(
|
||||
if optimize_ast {
|
||||
optimize(statements, &scope)
|
||||
} else {
|
||||
statements
|
||||
},
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
functions
|
||||
.into_iter()
|
||||
.map(|mut fn_def| {
|
||||
if optimize_ast {
|
||||
let pos = fn_def.body.position();
|
||||
let mut body = optimize(vec![fn_def.body], &scope);
|
||||
fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
|
||||
}
|
||||
Arc::new(fn_def)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
optimize_ast(engine, scope, statements, functions),
|
||||
#[cfg(feature = "no_optimize")]
|
||||
AST(
|
||||
statements,
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
functions.into_iter().map(Arc::new).collect(),
|
||||
),
|
||||
AST(statements, functions.into_iter().map(Arc::new).collect()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse<'a>(
|
||||
input: &mut Peekable<TokenIterator<'a>>,
|
||||
scope: &Scope,
|
||||
optimize_ast: bool,
|
||||
) -> Result<AST, ParseError> {
|
||||
parse_top_level(input, scope, optimize_ast)
|
||||
pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option<Expr>, Dynamic) {
|
||||
if value.is::<INT>() {
|
||||
let value2 = value.clone();
|
||||
(
|
||||
Some(Expr::IntegerConstant(
|
||||
*value.downcast::<INT>().expect("value should be INT"),
|
||||
pos,
|
||||
)),
|
||||
value2,
|
||||
)
|
||||
} else if value.is::<char>() {
|
||||
let value2 = value.clone();
|
||||
(
|
||||
Some(Expr::CharConstant(
|
||||
*value.downcast::<char>().expect("value should be char"),
|
||||
pos,
|
||||
)),
|
||||
value2,
|
||||
)
|
||||
} else if value.is::<String>() {
|
||||
let value2 = value.clone();
|
||||
(
|
||||
Some(Expr::StringConstant(
|
||||
*value.downcast::<String>().expect("value should be String"),
|
||||
pos,
|
||||
)),
|
||||
value2,
|
||||
)
|
||||
} else if value.is::<bool>() {
|
||||
let value2 = value.clone();
|
||||
(
|
||||
Some(
|
||||
if *value.downcast::<bool>().expect("value should be bool") {
|
||||
Expr::True(pos)
|
||||
} else {
|
||||
Expr::False(pos)
|
||||
},
|
||||
),
|
||||
value2,
|
||||
)
|
||||
} else {
|
||||
#[cfg(not(feature = "no_float"))]
|
||||
{
|
||||
if value.is::<FLOAT>() {
|
||||
let value2 = value.clone();
|
||||
return (
|
||||
Some(Expr::FloatConstant(
|
||||
*value.downcast::<FLOAT>().expect("value should be FLOAT"),
|
||||
pos,
|
||||
)),
|
||||
value2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(None, value)
|
||||
}
|
||||
}
|
||||
|
74
src/scope.rs
74
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<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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
@ -1,19 +1,19 @@
|
||||
use rhai::{Engine, EvalAltResult};
|
||||
use rhai::{Engine, EvalAltResult, INT};
|
||||
|
||||
#[test]
|
||||
fn test_constant() -> Result<(), EvalAltResult> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
assert_eq!(engine.eval::<i64>("const x = 123; x")?, 123);
|
||||
assert_eq!(engine.eval::<INT>("const x = 123; x")?, 123);
|
||||
|
||||
assert!(
|
||||
matches!(engine.eval::<i64>("const x = 123; x = 42;").expect_err("expects error"),
|
||||
matches!(engine.eval::<INT>("const x = 123; x = 42;").expect_err("expects error"),
|
||||
EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
assert!(
|
||||
matches!(engine.eval::<i64>("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"),
|
||||
matches!(engine.eval::<INT>("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"),
|
||||
EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x")
|
||||
);
|
||||
|
||||
|
@ -32,13 +32,13 @@ fn test_mismatched_op_custom_type() {
|
||||
.eval::<INT>("60 + new_ts()")
|
||||
.expect_err("expects error");
|
||||
|
||||
match r {
|
||||
#[cfg(feature = "only_i32")]
|
||||
EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)" => (),
|
||||
assert!(
|
||||
matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)")
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "only_i32"))]
|
||||
EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)" => (),
|
||||
|
||||
_ => panic!(),
|
||||
}
|
||||
assert!(
|
||||
matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)")
|
||||
);
|
||||
}
|
||||
|
32
tests/optimizer.rs
Normal file
32
tests/optimizer.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user