From b3a22d942a16bb0454b0b2622545862f7170721b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 14:30:44 +0800 Subject: [PATCH] Allow AST optimization based on external Scope. --- examples/repl.rs | 2 +- scripts/primes.rhai | 2 +- src/api.rs | 26 ++++++- src/optimize.rs | 17 +++- src/parser.rs | 47 +++++++++++- src/scope.rs | 183 +++++++++++++++++++++++++++++++++----------- 6 files changed, 222 insertions(+), 55 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 381bc265..7e32ec16 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -73,7 +73,7 @@ fn main() { } if let Err(err) = engine - .compile(&input) + .compile_with_scope(&scope, &input) .map_err(EvalAltResult::ErrorParsing) .and_then(|r| { ast = Some(r); diff --git a/scripts/primes.rhai b/scripts/primes.rhai index fae57bbb..d3892bce 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -1,6 +1,6 @@ // This is a script to calculate prime numbers. -let MAX_NUMBER_TO_CHECK = 10000; // 1229 primes +const MAX_NUMBER_TO_CHECK = 10000; // 1229 primes let prime_mask = []; prime_mask.pad(MAX_NUMBER_TO_CHECK, true); diff --git a/src/api.rs b/src/api.rs index cf03125f..401e4b77 100644 --- a/src/api.rs +++ b/src/api.rs @@ -100,8 +100,14 @@ impl<'e> Engine<'e> { /// Compile a string into an AST. pub fn compile(&self, input: &str) -> Result { + self.compile_with_scope(&Scope::new(), input) + } + + /// Compile a string into an AST using own scope. + /// 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(), self.optimize) + parse(&mut tokens_stream.peekable(), scope, self.optimize) } fn read_file(filename: &str) -> Result { @@ -117,8 +123,20 @@ impl<'e> Engine<'e> { /// Compile a file into an AST. pub fn compile_file(&self, filename: &str) -> Result { - Self::read_file(filename) - .and_then(|contents| self.compile(&contents).map_err(|err| err.into())) + self.compile_file_with_scope(&Scope::new(), filename) + } + + /// Compile a file into an AST using own scope. + /// The scope is useful for passing constants into the script for optimization. + pub fn compile_file_with_scope( + &self, + scope: &Scope, + filename: &str, + ) -> Result { + Self::read_file(filename).and_then(|contents| { + self.compile_with_scope(scope, &contents) + .map_err(|err| err.into()) + }) } /// Evaluate a file. @@ -241,7 +259,7 @@ impl<'e> Engine<'e> { ) -> Result<(), EvalAltResult> { let tokens_stream = lex(input); - let ast = parse(&mut tokens_stream.peekable(), self.optimize) + let ast = parse(&mut tokens_stream.peekable(), scope, self.optimize) .map_err(EvalAltResult::ErrorParsing)?; self.consume_ast_with_scope(scope, retain_functions, &ast) diff --git a/src/optimize.rs b/src/optimize.rs index 102839d9..95f36186 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,5 +1,6 @@ use crate::engine::KEYWORD_DUMP_AST; use crate::parser::{Expr, Stmt}; +use crate::scope::{Scope, ScopeEntry, VariableType}; struct State { changed: bool, @@ -330,13 +331,27 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } } -pub(crate) fn optimize(statements: Vec) -> Vec { +pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { let mut result = statements; loop { let mut state = State::new(); let num_statements = result.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(), + ) + }); + result = result .into_iter() .enumerate() diff --git a/src/parser.rs b/src/parser.rs index 0b0e0018..89021d39 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,8 @@ use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::{optimize::optimize, scope::VariableType}; +use crate::optimize::optimize; +use crate::scope::{Scope, VariableType}; use std::{ borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc, @@ -148,6 +149,42 @@ pub struct AST( #[cfg(not(feature = "no_function"))] 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 once and stored. During actually 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. + pub fn optimize(self, scope: &Scope) -> Self { + AST( + crate::optimize::optimize(self.0, scope), + #[cfg(not(feature = "no_function"))] + self.1 + .into_iter() + .map(|fn_def| { + let pos = fn_def.body.position(); + let body = optimize(vec![fn_def.body.clone()], scope) + .into_iter() + .next() + .unwrap_or_else(|| Stmt::Noop(pos)); + Arc::new(FnDef { + name: fn_def.name.clone(), + params: fn_def.params.clone(), + body, + pos: fn_def.pos, + }) + }) + .collect(), + ) + } +} + #[derive(Debug)] // Do not derive Clone because it is expensive pub struct FnDef { pub name: String, @@ -2043,6 +2080,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( input: &mut Peekable>, + scope: &Scope, optimize_ast: bool, ) -> Result { let mut statements = Vec::::new(); @@ -2073,7 +2111,7 @@ fn parse_top_level<'a>( return Ok(AST( if optimize_ast { - optimize(statements) + optimize(statements, &scope) } else { statements }, @@ -2083,7 +2121,7 @@ fn parse_top_level<'a>( .map(|mut fn_def| { if optimize_ast { let pos = fn_def.body.position(); - let mut body = optimize(vec![fn_def.body]); + let mut body = optimize(vec![fn_def.body], &scope); fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); } Arc::new(fn_def) @@ -2094,7 +2132,8 @@ fn parse_top_level<'a>( pub fn parse<'a>( input: &mut Peekable>, + scope: &Scope, optimize_ast: bool, ) -> Result { - parse_top_level(input, optimize_ast) + parse_top_level(input, scope, optimize_ast) } diff --git a/src/scope.rs b/src/scope.rs index b3eef176..e41b7b67 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,10 @@ //! Module that defines the `Scope` type representing a function call-stack scope. -use crate::any::{Any, Dynamic}; +use crate::any::{Any, AnyExt, Dynamic}; +use crate::parser::{Expr, Position, INT}; + +#[cfg(not(feature = "no_float"))] +use crate::parser::FLOAT; use std::borrow::Cow; @@ -10,6 +14,13 @@ pub enum VariableType { Constant, } +pub struct ScopeEntry<'a> { + pub name: Cow<'a, str>, + pub var_type: VariableType, + pub value: Dynamic, + pub expr: Option, +} + /// A type containing information about current scope. /// Useful for keeping state between `Engine` runs. /// @@ -31,7 +42,7 @@ pub enum VariableType { /// /// When searching for variables, newly-added variables are found before similarly-named but older variables, /// allowing for automatic _shadowing_ of variables. -pub struct Scope<'a>(Vec<(Cow<'a, str>, VariableType, Dynamic)>); +pub struct Scope<'a>(Vec>); impl<'a> Scope<'a> { /// Create a new Scope. @@ -50,32 +61,62 @@ impl<'a> Scope<'a> { } /// Add (push) a new variable to the Scope. - pub fn push>, T: Any>(&mut self, key: K, value: T) { - self.0 - .push((key.into(), VariableType::Normal, Box::new(value))); + pub fn push>, T: Any>(&mut self, name: K, value: T) { + let value = value.into_dynamic(); + + // Map into constant expressions + let (expr, value) = map_dynamic_to_expr(value); + + self.0.push(ScopeEntry { + name: name.into(), + var_type: VariableType::Normal, + value, + expr, + }); } /// Add (push) a new constant to the Scope. - pub fn push_constant>, T: Any>(&mut self, key: K, value: T) { - self.0 - .push((key.into(), VariableType::Constant, Box::new(value))); + pub fn push_constant>, T: Any>(&mut self, name: K, value: T) { + let value = value.into_dynamic(); + + // Map into constant expressions + let (expr, value) = map_dynamic_to_expr(value); + + self.0.push(ScopeEntry { + name: name.into(), + var_type: VariableType::Constant, + value, + expr, + }); } /// Add (push) a new variable with a `Dynamic` value to the Scope. pub(crate) fn push_dynamic>>( &mut self, - key: K, - val_type: VariableType, + name: K, + var_type: VariableType, value: Dynamic, ) { - self.0.push((key.into(), val_type, value)); + let (expr, value) = map_dynamic_to_expr(value); + + self.0.push(ScopeEntry { + name: name.into(), + var_type, + value, + expr, + }); } /// Remove (pop) the last variable from the Scope. pub fn pop(&mut self) -> Option<(String, VariableType, Dynamic)> { - self.0 - .pop() - .map(|(key, var_type, value)| (key.to_string(), var_type, value)) + self.0.pop().map( + |ScopeEntry { + name, + var_type, + value, + .. + }| (name.to_string(), var_type, value), + ) } /// Truncate (rewind) the Scope to a previous size. @@ -89,8 +130,18 @@ impl<'a> Scope<'a> { .iter() .enumerate() .rev() // Always search a Scope in reverse order - .find(|(_, (name, _, _))| name == key) - .map(|(i, (name, var_type, value))| (i, name.as_ref(), *var_type, value.clone())) + .find(|(_, ScopeEntry { name, .. })| name == key) + .map( + |( + i, + ScopeEntry { + name, + var_type, + value, + .. + }, + )| (i, name.as_ref(), *var_type, value.clone()), + ) } /// Get the value of a variable in the Scope, starting from the last. @@ -99,50 +150,37 @@ impl<'a> Scope<'a> { .iter() .enumerate() .rev() // Always search a Scope in reverse order - .find(|(_, (name, _, _))| name == key) - .and_then(|(_, (_, _, value))| value.downcast_ref::()) - .map(|value| value.clone()) + .find(|(_, ScopeEntry { name, .. })| name == key) + .and_then(|(_, ScopeEntry { value, .. })| value.downcast_ref::()) + .map(T::clone) } /// Get a mutable reference to a variable in the Scope. - pub(crate) fn get_mut(&mut self, key: &str, index: usize) -> &mut Dynamic { + pub(crate) fn get_mut(&mut self, name: &str, index: usize) -> &mut Dynamic { let entry = self.0.get_mut(index).expect("invalid index in Scope"); assert_ne!( - entry.1, + entry.var_type, VariableType::Constant, "get mut of constant variable" ); - assert_eq!(entry.0, key, "incorrect key at Scope entry"); + assert_eq!(entry.name, name, "incorrect key at Scope entry"); - &mut entry.2 + &mut entry.value } /// Get a mutable reference to a variable in the Scope and downcast it to a specific type #[cfg(not(feature = "no_index"))] - pub(crate) fn get_mut_by_type(&mut self, key: &str, index: usize) -> &mut T { - self.get_mut(key, index) + pub(crate) fn get_mut_by_type(&mut self, name: &str, index: usize) -> &mut T { + self.get_mut(name, index) .downcast_mut::() .expect("wrong type cast") } /// Get an iterator to variables in the Scope. - pub fn iter(&self) -> impl Iterator { - self.0 - .iter() - .rev() // Always search a Scope in reverse order - .map(|(key, var_type, value)| (key.as_ref(), *var_type, value)) + pub fn iter(&self) -> impl Iterator { + self.0.iter().rev() // Always search a Scope in reverse order } - - /* - /// Get a mutable iterator to variables in the Scope. - pub fn iter_mut(&mut self) -> impl Iterator { - self.0 - .iter_mut() - .rev() // Always search a Scope in reverse order - .map(|(key, value)| (key.as_ref(), value)) - } - */ } impl<'a, K> std::iter::Extend<(K, VariableType, Dynamic)> for Scope<'a> @@ -150,9 +188,66 @@ where K: Into>, { fn extend>(&mut self, iter: T) { - self.0.extend( - iter.into_iter() - .map(|(key, var_type, value)| (key.into(), var_type, value)), - ); + self.0 + .extend(iter.into_iter().map(|(name, var_type, value)| ScopeEntry { + name: name.into(), + var_type, + value, + expr: None, + })); + } +} + +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::FloatConstant( + *value.downcast::().expect("value should be FLOAT"), + 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 { + (None, value) } }