Allow AST optimization based on external Scope.

This commit is contained in:
Stephen Chung 2020-03-14 14:30:44 +08:00
parent 9844ae8665
commit b3a22d942a
6 changed files with 222 additions and 55 deletions

View File

@ -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);

View File

@ -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);

View File

@ -100,8 +100,14 @@ impl<'e> Engine<'e> {
/// Compile a string into an AST.
pub fn compile(&self, input: &str) -> Result<AST, ParseError> {
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<AST, ParseError> {
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<String, EvalAltResult> {
@ -117,8 +123,20 @@ impl<'e> Engine<'e> {
/// Compile a file into an AST.
pub fn compile_file(&self, filename: &str) -> Result<AST, EvalAltResult> {
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<AST, EvalAltResult> {
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)

View File

@ -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<Stmt>) -> Vec<Stmt> {
pub(crate) fn optimize(statements: Vec<Stmt>, scope: &Scope) -> Vec<Stmt> {
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()

View File

@ -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<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 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<TokenIterator<'a>>) -> Result<FnDef, ParseE
fn parse_top_level<'a>(
input: &mut Peekable<TokenIterator<'a>>,
scope: &Scope,
optimize_ast: bool,
) -> Result<AST, ParseError> {
let mut statements = Vec::<Stmt>::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<TokenIterator<'a>>,
scope: &Scope,
optimize_ast: bool,
) -> Result<AST, ParseError> {
parse_top_level(input, optimize_ast)
parse_top_level(input, scope, optimize_ast)
}

View File

@ -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<Expr>,
}
/// 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<ScopeEntry<'a>>);
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<K: Into<Cow<'a, str>>, T: Any>(&mut self, key: K, value: T) {
self.0
.push((key.into(), VariableType::Normal, Box::new(value)));
pub fn push<K: Into<Cow<'a, str>>, 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<K: Into<Cow<'a, str>>, T: Any>(&mut self, key: K, value: T) {
self.0
.push((key.into(), VariableType::Constant, Box::new(value)));
pub fn push_constant<K: Into<Cow<'a, str>>, 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<K: Into<Cow<'a, str>>>(
&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::<T>())
.map(|value| value.clone())
.find(|(_, ScopeEntry { name, .. })| name == key)
.and_then(|(_, ScopeEntry { value, .. })| value.downcast_ref::<T>())
.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<T: Any + Clone>(&mut self, key: &str, index: usize) -> &mut T {
self.get_mut(key, index)
pub(crate) fn get_mut_by_type<T: Any + Clone>(&mut self, name: &str, index: usize) -> &mut T {
self.get_mut(name, index)
.downcast_mut::<T>()
.expect("wrong type cast")
}
/// Get an iterator to variables in the Scope.
pub fn iter(&self) -> impl Iterator<Item = (&str, VariableType, &Dynamic)> {
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<Item = &ScopeEntry> {
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<Item = (&str, &mut Dynamic)> {
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<Cow<'a, str>>,
{
fn extend<T: IntoIterator<Item = (K, VariableType, Dynamic)>>(&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<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::<FLOAT>() {
let value2 = value.clone();
(
Some(Expr::FloatConstant(
*value.downcast::<FLOAT>().expect("value should be FLOAT"),
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 {
(None, value)
}
}