Merge branch 'constants'

This commit is contained in:
Stephen Chung 2020-03-14 14:33:56 +08:00
commit cb6171ebcd
12 changed files with 619 additions and 182 deletions

View File

@ -57,8 +57,8 @@ Related
Other cool projects to check out: Other cool projects to check out:
* [ChaiScript] - A strong inspiration for Rhai. An embedded scripting language for C++ that I helped created many moons ago, now being lead by my cousin. * [ChaiScript](http://chaiscript.com/) - A strong inspiration for Rhai. An embedded scripting language for C++ that I helped created many moons ago, now being lead by my cousin.
* You can also check out the list of [scripting languages for Rust] on [awesome-rust]. * You can also check out the list of [scripting languages for Rust](https://github.com/rust-unofficial/awesome-rust#scripting) on [awesome-rust](https://github.com/rust-unofficial/awesome-rust)
Examples Examples
-------- --------
@ -597,6 +597,17 @@ Variables in Rhai follow normal naming rules (i.e. must contain only ASCII lette
let x = 3; let x = 3;
``` ```
Constants
---------
Constants can be defined and are immutable. Constants follow the same naming rules as [variables](#variables).
```rust
const x = 42;
print(x * 2); // prints 84
x = 123; // <- syntax error - cannot assign to constant
```
Numbers Numbers
------- -------
@ -1108,10 +1119,12 @@ The above script optimizes to:
} }
``` ```
Constant propagation is used to remove dead code: Constants propagation is used to remove dead code:
```rust ```rust
if true || some_work() { print("done!"); } // since '||' short-circuits, 'some_work' is never called const abc = true;
if abc || some_work() { print("done!"); } // 'abc' is constant so it is replaced by 'true'...
if true || some_work() { print("done!"); } // since '||' short-circuits, 'some_work' is never called because the LHS is 'true'
if true { print("done!"); } // <-- the line above is equivalent to this if true { print("done!"); } // <-- the line above is equivalent to this
print("done!"); // <-- the line above is further simplified to this print("done!"); // <-- the line above is further simplified to this
// because the condition is always true // because the condition is always true
@ -1169,10 +1182,6 @@ engine.set_optimization(false); // turn off the optimizer
``` ```
[ChaiScript]: http://chaiscript.com/
[scripting languages for Rust]: https://github.com/rust-unofficial/awesome-rust#scripting
[awesome-rust]: https://github.com/rust-unofficial/awesome-rust
[`num-traits`]: https://crates.io/crates/num-traits/ [`num-traits`]: https://crates.io/crates/num-traits/
[`debug_msgs`]: #optional-features [`debug_msgs`]: #optional-features
[`unchecked`]: #optional-features [`unchecked`]: #optional-features

View File

@ -73,7 +73,7 @@ fn main() {
} }
if let Err(err) = engine if let Err(err) = engine
.compile(&input) .compile_with_scope(&scope, &input)
.map_err(EvalAltResult::ErrorParsing) .map_err(EvalAltResult::ErrorParsing)
.and_then(|r| { .and_then(|r| {
ast = Some(r); ast = Some(r);

View File

@ -1,6 +1,6 @@
// This is a script to calculate prime numbers. // 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 = []; let prime_mask = [];
prime_mask.pad(MAX_NUMBER_TO_CHECK, true); prime_mask.pad(MAX_NUMBER_TO_CHECK, true);

View File

@ -101,8 +101,14 @@ impl<'e> Engine<'e> {
/// Compile a string into an AST. /// Compile a string into an AST.
pub fn compile(&self, input: &str) -> Result<AST, ParseError> { 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); let tokens_stream = lex(input);
parse(&mut tokens_stream.peekable(), self.optimize) parse(&mut tokens_stream.peekable(), scope, self.optimize)
} }
fn read_file(path: PathBuf) -> Result<String, EvalAltResult> { fn read_file(path: PathBuf) -> Result<String, EvalAltResult> {
@ -118,8 +124,20 @@ impl<'e> Engine<'e> {
/// Compile a file into an AST. /// Compile a file into an AST.
pub fn compile_file(&self, path: PathBuf) -> Result<AST, EvalAltResult> { pub fn compile_file(&self, path: PathBuf) -> Result<AST, EvalAltResult> {
Self::read_file(path) self.compile_file_with_scope(&Scope::new(), path)
.and_then(|contents| self.compile(&contents).map_err(|err| err.into())) }
/// 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,
path: PathBuf,
) -> Result<AST, EvalAltResult> {
Self::read_file(path).and_then(|contents| {
self.compile_with_scope(scope, &contents)
.map_err(|err| err.into())
})
} }
/// Evaluate a file. /// Evaluate a file.
@ -235,7 +253,7 @@ impl<'e> Engine<'e> {
) -> Result<(), EvalAltResult> { ) -> Result<(), EvalAltResult> {
let tokens_stream = lex(input); let tokens_stream = lex(input);
let ast = parse(&mut tokens_stream.peekable(), self.optimize) let ast = parse(&mut tokens_stream.peekable(), scope, self.optimize)
.map_err(EvalAltResult::ErrorParsing)?; .map_err(EvalAltResult::ErrorParsing)?;
self.consume_ast_with_scope(scope, retain_functions, &ast) self.consume_ast_with_scope(scope, retain_functions, &ast)

View File

@ -3,7 +3,7 @@
use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::any::{Any, AnyExt, Dynamic, Variant};
use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt}; use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
use crate::result::EvalAltResult; use crate::result::EvalAltResult;
use crate::scope::Scope; use crate::scope::{Scope, VariableType};
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
use crate::INT; use crate::INT;
@ -148,7 +148,8 @@ impl Engine<'_> {
fn_def fn_def
.params .params
.iter() .iter()
.zip(args.iter().map(|x| (*x).into_dynamic())), .zip(args.iter().map(|x| (*x).into_dynamic()))
.map(|(name, value)| (name, VariableType::Normal, value)),
); );
// Evaluate // Evaluate
@ -255,7 +256,7 @@ impl Engine<'_> {
} }
// xxx.id // xxx.id
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -266,7 +267,7 @@ impl Engine<'_> {
Expr::Index(idx_lhs, idx_expr, idx_pos) => { Expr::Index(idx_lhs, idx_expr, idx_pos) => {
let (val, _) = match idx_lhs.as_ref() { let (val, _) = match idx_lhs.as_ref() {
// xxx.id[idx_expr] // xxx.id[idx_expr]
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
( (
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?, self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
@ -294,7 +295,7 @@ impl Engine<'_> {
// xxx.dot_lhs.rhs // xxx.dot_lhs.rhs
Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() { Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() {
// xxx.id.rhs // xxx.id.rhs
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -305,7 +306,7 @@ impl Engine<'_> {
Expr::Index(idx_lhs, idx_expr, idx_pos) => { Expr::Index(idx_lhs, idx_expr, idx_pos) => {
let (val, _) = match idx_lhs.as_ref() { let (val, _) = match idx_lhs.as_ref() {
// xxx.id[idx_expr].rhs // xxx.id[idx_expr].rhs
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
( (
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?, self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
@ -353,8 +354,8 @@ impl Engine<'_> {
) -> Result<Dynamic, EvalAltResult> { ) -> Result<Dynamic, EvalAltResult> {
match dot_lhs { match dot_lhs {
// id.??? // id.???
Expr::Identifier(id, pos) => { Expr::Variable(id, pos) => {
let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; let (src_idx, _, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs); let val = self.get_dot_val_helper(scope, target.as_mut(), dot_rhs);
// In case the expression mutated `target`, we need to update it back into the scope because it is cloned. // In case the expression mutated `target`, we need to update it back into the scope because it is cloned.
@ -400,11 +401,11 @@ impl Engine<'_> {
id: &str, id: &str,
map: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>, map: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>,
begin: Position, begin: Position,
) -> Result<(usize, T), EvalAltResult> { ) -> Result<(usize, VariableType, T), EvalAltResult> {
scope scope
.get(id) .get(id)
.ok_or_else(|| EvalAltResult::ErrorVariableNotFound(id.into(), begin)) .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(id.into(), begin))
.and_then(move |(idx, _, val)| map(val).map(|v| (idx, v))) .and_then(move |(idx, _, var_type, val)| map(val).map(|v| (idx, var_type, v)))
} }
/// Evaluate the value of an index (must evaluate to INT) /// Evaluate the value of an index (must evaluate to INT)
@ -481,13 +482,13 @@ impl Engine<'_> {
match lhs { match lhs {
// id[idx_expr] // id[idx_expr]
Expr::Identifier(id, _) => Self::search_scope( Expr::Variable(id, _) => Self::search_scope(
scope, scope,
&id, &id,
|val| self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos), |val| self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos),
lhs.position(), lhs.position(),
) )
.map(|(src_idx, (val, src_type))| { .map(|(src_idx, _, (val, src_type))| {
(src_type, Some((id.as_str(), src_idx)), idx as usize, val) (src_type, Some((id.as_str(), src_idx)), idx as usize, val)
}), }),
@ -585,7 +586,7 @@ impl Engine<'_> {
) -> Result<Dynamic, EvalAltResult> { ) -> Result<Dynamic, EvalAltResult> {
match dot_rhs { match dot_rhs {
// xxx.id // xxx.id
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let set_fn_name = format!("{}{}", FUNC_SETTER, id); let set_fn_name = format!("{}{}", FUNC_SETTER, id);
self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.as_mut()], None, *pos) self.call_fn_raw(&set_fn_name, vec![this_ptr, new_val.as_mut()], None, *pos)
@ -596,7 +597,7 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() { Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
// xxx.id[idx_expr] // xxx.id[idx_expr]
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -620,7 +621,7 @@ impl Engine<'_> {
// xxx.lhs.{...} // xxx.lhs.{...}
Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { Expr::Dot(lhs, rhs, _) => match lhs.as_ref() {
// xxx.id.rhs // xxx.id.rhs
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -640,7 +641,7 @@ impl Engine<'_> {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() { Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
// xxx.id[idx_expr].rhs // xxx.id[idx_expr].rhs
Expr::Identifier(id, pos) => { Expr::Property(id, pos) => {
let get_fn_name = format!("{}{}", FUNC_GETTER, id); let get_fn_name = format!("{}{}", FUNC_GETTER, id);
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos) self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
@ -702,11 +703,23 @@ impl Engine<'_> {
dot_rhs: &Expr, dot_rhs: &Expr,
new_val: Dynamic, new_val: Dynamic,
val_pos: Position, val_pos: Position,
op_pos: Position,
) -> Result<Dynamic, EvalAltResult> { ) -> Result<Dynamic, EvalAltResult> {
match dot_lhs { match dot_lhs {
// id.??? // id.???
Expr::Identifier(id, pos) => { Expr::Variable(id, pos) => {
let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?; let (src_idx, var_type, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
match var_type {
VariableType::Constant => {
return Err(EvalAltResult::ErrorAssignmentToConstant(
id.to_string(),
op_pos,
))
}
_ => (),
}
let val = let val =
self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos);
@ -758,9 +771,10 @@ impl Engine<'_> {
Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()), Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()),
Expr::StringConstant(s, _) => Ok(s.into_dynamic()), Expr::StringConstant(s, _) => Ok(s.into_dynamic()),
Expr::CharConstant(c, _) => Ok(c.into_dynamic()), Expr::CharConstant(c, _) => Ok(c.into_dynamic()),
Expr::Identifier(id, pos) => { Expr::Variable(id, pos) => {
Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val) Self::search_scope(scope, id, Ok, *pos).map(|(_, _, val)| val)
} }
Expr::Property(_, _) => panic!("unexpected property."),
// lhs[idx_expr] // lhs[idx_expr]
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
@ -775,19 +789,21 @@ impl Engine<'_> {
Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt), Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt),
// lhs = rhs // lhs = rhs
Expr::Assignment(lhs, rhs, _) => { Expr::Assignment(lhs, rhs, op_pos) => {
let rhs_val = self.eval_expr(scope, rhs)?; let rhs_val = self.eval_expr(scope, rhs)?;
match lhs.as_ref() { match lhs.as_ref() {
// name = rhs // name = rhs
Expr::Identifier(name, pos) => { Expr::Variable(name, pos) => match scope.get(name) {
if let Some((idx, _, _)) = scope.get(name) { Some((idx, _, VariableType::Normal, _)) => {
*scope.get_mut(name, idx) = rhs_val; *scope.get_mut(name, idx) = rhs_val;
Ok(().into_dynamic()) Ok(().into_dynamic())
} else {
Err(EvalAltResult::ErrorVariableNotFound(name.clone(), *pos))
}
} }
Some((_, _, VariableType::Constant, _)) => Err(
EvalAltResult::ErrorAssignmentToConstant(name.to_string(), *op_pos),
),
_ => Err(EvalAltResult::ErrorVariableNotFound(name.clone(), *pos)),
},
// idx_lhs[idx_expr] = rhs // idx_lhs[idx_expr] = rhs
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
@ -814,9 +830,15 @@ impl Engine<'_> {
// dot_lhs.dot_rhs = rhs // dot_lhs.dot_rhs = rhs
Expr::Dot(dot_lhs, dot_rhs, _) => { Expr::Dot(dot_lhs, dot_rhs, _) => {
self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position()) self.set_dot_val(scope, dot_lhs, dot_rhs, rhs_val, rhs.position(), *op_pos)
} }
// Error assignment to constant
expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant(
expr.get_value_str(),
lhs.position(),
)),
// Syntax error // Syntax error
_ => Err(EvalAltResult::ErrorAssignmentToUnknownLHS(lhs.position())), _ => Err(EvalAltResult::ErrorAssignmentToUnknownLHS(lhs.position())),
} }
@ -1045,7 +1067,7 @@ impl Engine<'_> {
// Let statement // Let statement
Stmt::Let(name, Some(expr), _) => { Stmt::Let(name, Some(expr), _) => {
let val = self.eval_expr(scope, expr)?; let val = self.eval_expr(scope, expr)?;
scope.push_dynamic(name.clone(), val); scope.push_dynamic(name.clone(), VariableType::Normal, val);
Ok(().into_dynamic()) Ok(().into_dynamic())
} }
@ -1053,6 +1075,15 @@ impl Engine<'_> {
scope.push(name.clone(), ()); scope.push(name.clone(), ());
Ok(().into_dynamic()) Ok(().into_dynamic())
} }
// Const statement
Stmt::Const(name, expr, _) if expr.is_constant() => {
let val = self.eval_expr(scope, expr)?;
scope.push_dynamic(name.clone(), VariableType::Constant, val);
Ok(().into_dynamic())
}
Stmt::Const(_, _, _) => panic!("constant expression not constant!"),
} }
} }

View File

@ -67,6 +67,8 @@ pub enum ParseErrorType {
MalformedCallExpr(String), MalformedCallExpr(String),
/// An expression in indexing brackets `[]` has syntax error. /// An expression in indexing brackets `[]` has syntax error.
MalformedIndexExpr(String), MalformedIndexExpr(String),
/// Invalid expression assigned to constant.
ForbiddenConstantExpr(String),
/// Missing a variable name after the `let` keyword. /// Missing a variable name after the `let` keyword.
VarExpectsIdentifier, VarExpectsIdentifier,
/// Defining a function `fn` in an appropriate place (e.g. inside another function). /// Defining a function `fn` in an appropriate place (e.g. inside another function).
@ -77,6 +79,10 @@ pub enum ParseErrorType {
FnMissingParams(String), FnMissingParams(String),
/// Assignment to an inappropriate LHS (left-hand-side) expression. /// Assignment to an inappropriate LHS (left-hand-side) expression.
AssignmentToInvalidLHS, AssignmentToInvalidLHS,
/// Assignment to a copy of a value.
AssignmentToCopy,
/// Assignment to an a constant variable.
AssignmentToConstant(String),
} }
/// Error when parsing a script. /// Error when parsing a script.
@ -112,11 +118,14 @@ impl Error for ParseError {
ParseErrorType::MissingRightBracket(_) => "Expecting ']'", ParseErrorType::MissingRightBracket(_) => "Expecting ']'",
ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments",
ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression",
ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant",
ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable", ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable",
ParseErrorType::FnMissingName => "Expecting name in function declaration", ParseErrorType::FnMissingName => "Expecting name in function declaration",
ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration",
ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function",
ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression because it will only be changing a copy of the value" ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression",
ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value",
ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable."
} }
} }
@ -133,6 +142,9 @@ impl fmt::Display for ParseError {
| ParseErrorType::MalformedCallExpr(ref s) => { | ParseErrorType::MalformedCallExpr(ref s) => {
write!(f, "{}", if s.is_empty() { self.description() } else { s })? write!(f, "{}", if s.is_empty() { self.description() } else { s })?
} }
ParseErrorType::ForbiddenConstantExpr(ref s) => {
write!(f, "Expecting a constant to assign to '{}'", s)?
}
ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.description(), s)?, ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.description(), s)?,
ParseErrorType::FnMissingParams(ref s) => { ParseErrorType::FnMissingParams(ref s) => {
write!(f, "Expecting parameters for function '{}'", s)? write!(f, "Expecting parameters for function '{}'", s)?
@ -142,6 +154,12 @@ impl fmt::Display for ParseError {
| ParseErrorType::MissingRightBracket(ref s) => { | ParseErrorType::MissingRightBracket(ref s) => {
write!(f, "{} for {}", self.description(), s)? write!(f, "{} for {}", self.description(), s)?
} }
ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => {
write!(f, "{}", self.description())?
}
ParseErrorType::AssignmentToConstant(ref s) => {
write!(f, "Cannot assign to constant '{}'", s)?
}
_ => write!(f, "{}", self.description())?, _ => write!(f, "{}", self.description())?,
} }

View File

@ -1,13 +1,52 @@
use crate::engine::KEYWORD_DUMP_AST; use crate::engine::KEYWORD_DUMP_AST;
use crate::parser::{Expr, Stmt}; use crate::parser::{Expr, Stmt};
use crate::scope::{Scope, ScopeEntry, VariableType};
fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt { struct State {
changed: bool,
constants: Vec<(String, Expr)>,
}
impl State {
pub fn new() -> Self {
State {
changed: false,
constants: vec![],
}
}
pub fn set_dirty(&mut self) {
self.changed = true;
}
pub fn is_dirty(&self) -> bool {
self.changed
}
pub fn contains_constant(&self, name: &str) -> bool {
self.constants.iter().any(|(n, _)| n == name)
}
pub fn restore_constants(&mut self, len: usize) {
self.constants.truncate(len)
}
pub fn push_constant(&mut self, name: &str, value: Expr) {
self.constants.push((name.to_string(), value))
}
pub fn find_constant(&self, name: &str) -> Option<&Expr> {
for (n, expr) in self.constants.iter().rev() {
if n == name {
return Some(expr);
}
}
None
}
}
fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
match stmt { match stmt {
Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => { Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => {
*changed = true; state.set_dirty();
let pos = expr.position(); let pos = expr.position();
let expr = optimize_expr(*expr, changed); let expr = optimize_expr(*expr, state);
match expr { match expr {
Expr::False(_) | Expr::True(_) => Stmt::Noop(stmt1.position()), Expr::False(_) | Expr::True(_) => Stmt::Noop(stmt1.position()),
@ -25,24 +64,24 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
Stmt::IfElse(expr, stmt1, None) => match *expr { Stmt::IfElse(expr, stmt1, None) => match *expr {
Expr::False(pos) => { Expr::False(pos) => {
*changed = true; state.set_dirty();
Stmt::Noop(pos) Stmt::Noop(pos)
} }
Expr::True(_) => optimize_stmt(*stmt1, changed, true), Expr::True(_) => optimize_stmt(*stmt1, state, true),
expr => Stmt::IfElse( expr => Stmt::IfElse(
Box::new(optimize_expr(expr, changed)), Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*stmt1, changed, true)), Box::new(optimize_stmt(*stmt1, state, true)),
None, None,
), ),
}, },
Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr { Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr {
Expr::False(_) => optimize_stmt(*stmt2, changed, true), Expr::False(_) => optimize_stmt(*stmt2, state, true),
Expr::True(_) => optimize_stmt(*stmt1, changed, true), Expr::True(_) => optimize_stmt(*stmt1, state, true),
expr => Stmt::IfElse( expr => Stmt::IfElse(
Box::new(optimize_expr(expr, changed)), Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*stmt1, changed, true)), Box::new(optimize_stmt(*stmt1, state, true)),
match optimize_stmt(*stmt2, changed, true) { match optimize_stmt(*stmt2, state, true) {
stmt if stmt.is_noop() => None, stmt if stmt.is_noop() => None,
stmt => Some(Box::new(stmt)), stmt => Some(Box::new(stmt)),
}, },
@ -51,38 +90,45 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
Stmt::While(expr, stmt) => match *expr { Stmt::While(expr, stmt) => match *expr {
Expr::False(pos) => { Expr::False(pos) => {
*changed = true; state.set_dirty();
Stmt::Noop(pos) Stmt::Noop(pos)
} }
Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed, false))), Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))),
expr => Stmt::While( expr => Stmt::While(
Box::new(optimize_expr(expr, changed)), Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*stmt, changed, false)), Box::new(optimize_stmt(*stmt, state, false)),
), ),
}, },
Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, changed, false))), Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))),
Stmt::For(id, expr, stmt) => Stmt::For( Stmt::For(id, expr, stmt) => Stmt::For(
id, id,
Box::new(optimize_expr(*expr, changed)), Box::new(optimize_expr(*expr, state)),
Box::new(optimize_stmt(*stmt, changed, false)), Box::new(optimize_stmt(*stmt, state, false)),
), ),
Stmt::Let(id, Some(expr), pos) => { Stmt::Let(id, Some(expr), pos) => {
Stmt::Let(id, Some(Box::new(optimize_expr(*expr, changed))), pos) Stmt::Let(id, Some(Box::new(optimize_expr(*expr, state))), pos)
} }
Stmt::Let(_, None, _) => stmt, Stmt::Let(_, None, _) => stmt,
Stmt::Block(statements, pos) => { Stmt::Block(statements, pos) => {
let orig_len = statements.len(); let orig_len = statements.len();
let orig_constants = state.constants.len();
let mut result: Vec<_> = statements let mut result: Vec<_> = statements
.into_iter() // For each statement .into_iter() // For each statement
.rev() // Scan in reverse .map(|stmt| {
.map(|s| optimize_stmt(s, changed, preserve_result)) // Optimize the statement if let Stmt::Const(name, value, pos) = stmt {
state.push_constant(&name, *value);
state.set_dirty();
Stmt::Noop(pos) // No need to keep constants
} else {
optimize_stmt(stmt, state, preserve_result) // Optimize the statement
}
})
.enumerate() .enumerate()
.filter(|(i, s)| s.is_op() || (preserve_result && *i == 0)) // Remove no-op's but leave the last one if we need the result .filter(|(i, stmt)| stmt.is_op() || (preserve_result && *i == orig_len - 1)) // Remove no-op's but leave the last one if we need the result
.map(|(_, s)| s) .map(|(_, stmt)| stmt)
.rev()
.collect(); .collect();
// Remove all raw expression statements that are pure except for the very last statement // Remove all raw expression statements that are pure except for the very last statement
@ -123,59 +169,61 @@ fn optimize_stmt(stmt: Stmt, changed: &mut bool, preserve_result: bool) -> Stmt
.into_iter() .into_iter()
.rev() .rev()
.enumerate() .enumerate()
.map(|(i, s)| optimize_stmt(s, changed, i == 0)) // Optimize all other statements again .map(|(i, s)| optimize_stmt(s, state, i == 0)) // Optimize all other statements again
.rev() .rev()
.collect(); .collect();
} }
*changed = *changed || orig_len != result.len(); if orig_len != result.len() {
state.set_dirty();
}
state.restore_constants(orig_constants);
match result[..] { match result[..] {
// No statements in block - change to No-op // No statements in block - change to No-op
[] => { [] => {
*changed = true; state.set_dirty();
Stmt::Noop(pos) Stmt::Noop(pos)
} }
// Only one statement - promote // Only one statement - promote
[_] => { [_] => {
*changed = true; state.set_dirty();
result.remove(0) result.remove(0)
} }
_ => Stmt::Block(result, pos), _ => Stmt::Block(result, pos),
} }
} }
Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, changed))), Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, state))),
Stmt::ReturnWithVal(Some(expr), is_return, pos) => Stmt::ReturnWithVal( Stmt::ReturnWithVal(Some(expr), is_return, pos) => {
Some(Box::new(optimize_expr(*expr, changed))), Stmt::ReturnWithVal(Some(Box::new(optimize_expr(*expr, state))), is_return, pos)
is_return, }
pos,
),
stmt => stmt, stmt => stmt,
} }
} }
fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr { fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
match expr { match expr {
Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, changed, true) { Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) {
Stmt::Noop(_) => { Stmt::Noop(_) => {
*changed = true; state.set_dirty();
Expr::Unit(pos) Expr::Unit(pos)
} }
Stmt::Expr(expr) => { Stmt::Expr(expr) => {
*changed = true; state.set_dirty();
*expr *expr
} }
stmt => Expr::Stmt(Box::new(stmt), pos), stmt => Expr::Stmt(Box::new(stmt), pos),
}, },
Expr::Assignment(id, expr, pos) => { Expr::Assignment(id, expr, pos) => {
Expr::Assignment(id, Box::new(optimize_expr(*expr, changed)), pos) Expr::Assignment(id, Box::new(optimize_expr(*expr, state)), pos)
} }
Expr::Dot(lhs, rhs, pos) => Expr::Dot( Expr::Dot(lhs, rhs, pos) => Expr::Dot(
Box::new(optimize_expr(*lhs, changed)), Box::new(optimize_expr(*lhs, state)),
Box::new(optimize_expr(*rhs, changed)), Box::new(optimize_expr(*rhs, state)),
pos, pos,
), ),
@ -186,12 +234,12 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
{ {
// Array where everything is a pure - promote the indexed item. // Array where everything is a pure - promote the indexed item.
// All other items can be thrown away. // All other items can be thrown away.
*changed = true; state.set_dirty();
items.remove(i as usize) items.remove(i as usize)
} }
(lhs, rhs) => Expr::Index( (lhs, rhs) => Expr::Index(
Box::new(optimize_expr(lhs, changed)), Box::new(optimize_expr(lhs, state)),
Box::new(optimize_expr(rhs, changed)), Box::new(optimize_expr(rhs, state)),
pos, pos,
), ),
}, },
@ -204,10 +252,12 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
let items: Vec<_> = items let items: Vec<_> = items
.into_iter() .into_iter()
.map(|expr| optimize_expr(expr, changed)) .map(|expr| optimize_expr(expr, state))
.collect(); .collect();
*changed = *changed || orig_len != items.len(); if orig_len != items.len() {
state.set_dirty();
}
Expr::Array(items, pos) Expr::Array(items, pos)
} }
@ -217,38 +267,38 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
Expr::And(lhs, rhs) => match (*lhs, *rhs) { Expr::And(lhs, rhs) => match (*lhs, *rhs) {
(Expr::True(_), rhs) => { (Expr::True(_), rhs) => {
*changed = true; state.set_dirty();
rhs rhs
} }
(Expr::False(pos), _) => { (Expr::False(pos), _) => {
*changed = true; state.set_dirty();
Expr::False(pos) Expr::False(pos)
} }
(lhs, Expr::True(_)) => { (lhs, Expr::True(_)) => {
*changed = true; state.set_dirty();
lhs lhs
} }
(lhs, rhs) => Expr::And( (lhs, rhs) => Expr::And(
Box::new(optimize_expr(lhs, changed)), Box::new(optimize_expr(lhs, state)),
Box::new(optimize_expr(rhs, changed)), Box::new(optimize_expr(rhs, state)),
), ),
}, },
Expr::Or(lhs, rhs) => match (*lhs, *rhs) { Expr::Or(lhs, rhs) => match (*lhs, *rhs) {
(Expr::False(_), rhs) => { (Expr::False(_), rhs) => {
*changed = true; state.set_dirty();
rhs rhs
} }
(Expr::True(pos), _) => { (Expr::True(pos), _) => {
*changed = true; state.set_dirty();
Expr::True(pos) Expr::True(pos)
} }
(lhs, Expr::False(_)) => { (lhs, Expr::False(_)) => {
*changed = true; state.set_dirty();
lhs lhs
} }
(lhs, rhs) => Expr::Or( (lhs, rhs) => Expr::Or(
Box::new(optimize_expr(lhs, changed)), Box::new(optimize_expr(lhs, state)),
Box::new(optimize_expr(rhs, changed)), Box::new(optimize_expr(rhs, state)),
), ),
}, },
@ -258,41 +308,69 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
Expr::FunctionCall(id, args, def_value, pos) => { Expr::FunctionCall(id, args, def_value, pos) => {
let orig_len = args.len(); let orig_len = args.len();
let args: Vec<_> = args let args: Vec<_> = args.into_iter().map(|a| optimize_expr(a, state)).collect();
.into_iter()
.map(|a| optimize_expr(a, changed))
.collect();
*changed = *changed || orig_len != args.len(); if orig_len != args.len() {
state.set_dirty();
}
Expr::FunctionCall(id, args, def_value, pos) Expr::FunctionCall(id, args, def_value, pos)
} }
Expr::Variable(ref name, _) if state.contains_constant(name) => {
state.set_dirty();
// Replace constant with value
state
.find_constant(name)
.expect("can't find constant in scope!")
.clone()
}
expr => expr, expr => 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; let mut result = statements;
loop { loop {
let mut changed = false; 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 result = result
.into_iter() .into_iter()
.rev() // Scan in reverse
.enumerate() .enumerate()
.map(|(i, stmt)| { .map(|(i, stmt)| {
if let Stmt::Const(name, value, _) = &stmt {
// Load constants
state.push_constant(name, value.as_ref().clone());
stmt // Keep it in the top scope
} else {
// Keep all variable declarations at this level // Keep all variable declarations at this level
let keep = stmt.is_var(); // and always keep the last return value
let keep = stmt.is_var() || i == num_statements - 1;
// Always keep the last return value optimize_stmt(stmt, &mut state, keep)
optimize_stmt(stmt, &mut changed, keep || i == 0) }
}) })
.rev()
.collect(); .collect();
if !changed { if !state.is_dirty() {
break; break;
} }
} }

View File

@ -3,6 +3,7 @@
use crate::any::Dynamic; use crate::any::Dynamic;
use crate::error::{LexError, ParseError, ParseErrorType}; use crate::error::{LexError, ParseError, ParseErrorType};
use crate::optimize::optimize; use crate::optimize::optimize;
use crate::scope::{Scope, VariableType};
use std::{ use std::{
borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc, borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc,
@ -148,6 +149,42 @@ pub struct AST(
#[cfg(not(feature = "no_function"))] pub(crate) Vec<Arc<FnDef>>, #[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 #[derive(Debug)] // Do not derive Clone because it is expensive
pub struct FnDef { pub struct FnDef {
pub name: String, pub name: String,
@ -179,6 +216,7 @@ pub enum Stmt {
Loop(Box<Stmt>), Loop(Box<Stmt>),
For(String, Box<Expr>, Box<Stmt>), For(String, Box<Expr>, Box<Stmt>),
Let(String, Option<Box<Expr>>, Position), Let(String, Option<Box<Expr>>, Position),
Const(String, Box<Expr>, Position),
Block(Vec<Stmt>, Position), Block(Vec<Stmt>, Position),
Expr(Box<Expr>), Expr(Box<Expr>),
Break(Position), Break(Position),
@ -211,6 +249,7 @@ impl Stmt {
match self { match self {
Stmt::Noop(pos) Stmt::Noop(pos)
| Stmt::Let(_, _, pos) | Stmt::Let(_, _, pos)
| Stmt::Const(_, _, pos)
| Stmt::Block(_, pos) | Stmt::Block(_, pos)
| Stmt::Break(pos) | Stmt::Break(pos)
| Stmt::ReturnWithVal(_, _, pos) => *pos, | Stmt::ReturnWithVal(_, _, pos) => *pos,
@ -225,7 +264,8 @@ pub enum Expr {
IntegerConstant(INT, Position), IntegerConstant(INT, Position),
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
FloatConstant(FLOAT, Position), FloatConstant(FLOAT, Position),
Identifier(String, Position), Variable(String, Position),
Property(String, Position),
CharConstant(char, Position), CharConstant(char, Position),
StringConstant(String, Position), StringConstant(String, Position),
Stmt(Box<Stmt>, Position), Stmt(Box<Stmt>, Position),
@ -242,12 +282,29 @@ pub enum Expr {
} }
impl Expr { impl Expr {
pub fn get_value_str(&self) -> String {
match self {
Expr::IntegerConstant(i, _) => i.to_string(),
Expr::CharConstant(c, _) => c.to_string(),
Expr::StringConstant(_, _) => "string".to_string(),
Expr::True(_) => "true".to_string(),
Expr::False(_) => "false".to_string(),
Expr::Unit(_) => "()".to_string(),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(f, _) => f.to_string(),
_ => "".to_string(),
}
}
pub fn position(&self) -> Position { pub fn position(&self) -> Position {
match self { match self {
Expr::IntegerConstant(_, pos) Expr::IntegerConstant(_, pos)
| Expr::Identifier(_, pos)
| Expr::CharConstant(_, pos) | Expr::CharConstant(_, pos)
| Expr::StringConstant(_, pos) | Expr::StringConstant(_, pos)
| Expr::Variable(_, pos)
| Expr::Property(_, pos)
| Expr::Stmt(_, pos) | Expr::Stmt(_, pos)
| Expr::FunctionCall(_, _, _, pos) | Expr::FunctionCall(_, _, _, pos)
| Expr::Array(_, pos) | Expr::Array(_, pos)
@ -273,7 +330,7 @@ impl Expr {
match self { match self {
Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure), Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure),
Expr::And(x, y) | Expr::Or(x, y) | Expr::Index(x, y, _) => x.is_pure() && y.is_pure(), Expr::And(x, y) | Expr::Or(x, y) | Expr::Index(x, y, _) => x.is_pure() && y.is_pure(),
expr => expr.is_constant() || expr.is_identifier(), expr => expr.is_constant() || expr.is_variable(),
} }
} }
@ -292,9 +349,17 @@ impl Expr {
_ => false, _ => false,
} }
} }
pub fn is_identifier(&self) -> bool {
pub fn is_variable(&self) -> bool {
match self { match self {
Expr::Identifier(_, _) => true, Expr::Variable(_, _) => true,
_ => false,
}
}
pub fn is_property(&self) -> bool {
match self {
Expr::Property(_, _) => true,
_ => false, _ => false,
} }
} }
@ -328,6 +393,7 @@ pub enum Token {
True, True,
False, False,
Let, Let,
Const,
If, If,
Else, Else,
While, While,
@ -402,6 +468,7 @@ impl Token {
True => "true", True => "true",
False => "false", False => "false",
Let => "let", Let => "let",
Const => "const",
If => "if", If => "if",
Else => "else", Else => "else",
While => "while", While => "while",
@ -829,6 +896,7 @@ impl<'a> TokenIterator<'a> {
"true" => Token::True, "true" => Token::True,
"false" => Token::False, "false" => Token::False,
"let" => Token::Let, "let" => Token::Let,
"const" => Token::Const,
"if" => Token::If, "if" => Token::If,
"else" => Token::Else, "else" => Token::Else,
"while" => Token::While, "while" => Token::While,
@ -1360,10 +1428,10 @@ fn parse_ident_expr<'a>(
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Some(&(Token::LeftBracket, pos)) => { Some(&(Token::LeftBracket, pos)) => {
input.next(); input.next();
parse_index_expr(Box::new(Expr::Identifier(id, begin)), input, pos) parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos)
} }
Some(_) => Ok(Expr::Identifier(id, begin)), Some(_) => Ok(Expr::Variable(id, begin)),
None => Ok(Expr::Identifier(id, Position::eof())), None => Ok(Expr::Variable(id, Position::eof())),
} }
} }
@ -1522,37 +1590,69 @@ fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pars
} }
fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result<Expr, ParseError> { fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result<Expr, ParseError> {
fn valid_assignment_chain(expr: &Expr) -> (bool, Position) { fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option<ParseError> {
match expr { match expr {
Expr::Identifier(_, pos) => (true, *pos), Expr::Variable(_, _) => {
assert!(is_top, "property expected but gets variable");
None
}
Expr::Property(_, _) => {
assert!(!is_top, "variable expected but gets property");
None
}
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) if idx_lhs.is_identifier() => (true, idx_lhs.position()), Expr::Index(idx_lhs, _, _) if idx_lhs.is_variable() => {
assert!(is_top, "property expected but gets variable");
None
}
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) => (false, idx_lhs.position()), Expr::Index(idx_lhs, _, _) if idx_lhs.is_property() => {
assert!(!is_top, "variable expected but gets property");
None
}
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) if is_top => valid_assignment_chain(idx_lhs, true),
#[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) if !is_top => Some(ParseError::new(
ParseErrorType::AssignmentToInvalidLHS,
idx_lhs.position(),
)),
Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() {
Expr::Identifier(_, _) => valid_assignment_chain(dot_rhs), Expr::Variable(_, _) if is_top => valid_assignment_chain(dot_rhs, false),
Expr::Property(_, _) if !is_top => valid_assignment_chain(dot_rhs, false),
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) if idx_lhs.is_identifier() => { Expr::Index(idx_lhs, _, _)
valid_assignment_chain(dot_rhs) if (idx_lhs.is_variable() && is_top) || (idx_lhs.is_property() && !is_top) =>
{
valid_assignment_chain(dot_rhs, false)
} }
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(idx_lhs, _, _) => (false, idx_lhs.position()), Expr::Index(idx_lhs, _, _) => Some(ParseError::new(
ParseErrorType::AssignmentToCopy,
idx_lhs.position(),
)),
_ => (false, dot_lhs.position()), expr => panic!("unexpected dot expression {:#?}", expr),
}, },
_ => (false, expr.position()), _ => Some(ParseError::new(
ParseErrorType::AssignmentToInvalidLHS,
expr.position(),
)),
} }
} }
//println!("{:#?} = {:#?}", lhs, rhs); //println!("{:#?} = {:#?}", lhs, rhs);
match valid_assignment_chain(&lhs) { match valid_assignment_chain(&lhs, true) {
(true, _) => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), None => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)),
(false, pos) => Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)), Some(err) => Err(err),
} }
} }
@ -1619,7 +1719,28 @@ fn parse_binary_op<'a>(
Token::PlusAssign => parse_op_assignment("+", current_lhs, rhs, pos)?, Token::PlusAssign => parse_op_assignment("+", current_lhs, rhs, pos)?,
Token::MinusAssign => parse_op_assignment("-", current_lhs, rhs, pos)?, Token::MinusAssign => parse_op_assignment("-", current_lhs, rhs, pos)?,
Token::Period => Expr::Dot(Box::new(current_lhs), Box::new(rhs), pos), Token::Period => {
fn change_var_to_property(expr: Expr) -> Expr {
match expr {
Expr::Dot(lhs, rhs, pos) => Expr::Dot(
Box::new(change_var_to_property(*lhs)),
Box::new(change_var_to_property(*rhs)),
pos,
),
Expr::Index(lhs, idx, pos) => {
Expr::Index(Box::new(change_var_to_property(*lhs)), idx, pos)
}
Expr::Variable(s, pos) => Expr::Property(s, pos),
expr => expr,
}
}
Expr::Dot(
Box::new(current_lhs),
Box::new(change_var_to_property(rhs)),
pos,
)
}
// Comparison operators default to false when passed invalid operands // Comparison operators default to false when passed invalid operands
Token::EqualsTo => Expr::FunctionCall( Token::EqualsTo => Expr::FunctionCall(
@ -1763,7 +1884,10 @@ fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
Ok(Stmt::For(name, Box::new(expr), Box::new(body))) Ok(Stmt::For(name, Box::new(expr), Box::new(body)))
} }
fn parse_var<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> { fn parse_var<'a>(
input: &mut Peekable<TokenIterator<'a>>,
var_type: VariableType,
) -> Result<Stmt, ParseError> {
let pos = match input.next() { let pos = match input.next() {
Some((_, tok_pos)) => tok_pos, Some((_, tok_pos)) => tok_pos,
_ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())),
@ -1779,7 +1903,19 @@ fn parse_var<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
Some(&(Token::Equals, _)) => { Some(&(Token::Equals, _)) => {
input.next(); input.next();
let init_value = parse_expr(input)?; let init_value = parse_expr(input)?;
Ok(Stmt::Let(name, Some(Box::new(init_value)), pos))
match var_type {
VariableType::Normal => Ok(Stmt::Let(name, Some(Box::new(init_value)), pos)),
VariableType::Constant if init_value.is_constant() => {
Ok(Stmt::Const(name, Box::new(init_value), pos))
}
// Constants require a constant expression
VariableType::Constant => Err(ParseError(
PERR::ForbiddenConstantExpr(name.to_string()),
init_value.position(),
)),
}
} }
_ => Ok(Stmt::Let(name, None, pos)), _ => Ok(Stmt::Let(name, None, pos)),
} }
@ -1867,7 +2003,8 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
} }
} }
Some(&(Token::LeftBrace, _)) => parse_block(input), Some(&(Token::LeftBrace, _)) => parse_block(input),
Some(&(Token::Let, _)) => parse_var(input), Some(&(Token::Let, _)) => parse_var(input, VariableType::Normal),
Some(&(Token::Const, _)) => parse_var(input, VariableType::Constant),
_ => parse_expr_stmt(input), _ => parse_expr_stmt(input),
} }
} }
@ -1943,6 +2080,7 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
fn parse_top_level<'a>( fn parse_top_level<'a>(
input: &mut Peekable<TokenIterator<'a>>, input: &mut Peekable<TokenIterator<'a>>,
scope: &Scope,
optimize_ast: bool, optimize_ast: bool,
) -> Result<AST, ParseError> { ) -> Result<AST, ParseError> {
let mut statements = Vec::<Stmt>::new(); let mut statements = Vec::<Stmt>::new();
@ -1973,7 +2111,7 @@ fn parse_top_level<'a>(
return Ok(AST( return Ok(AST(
if optimize_ast { if optimize_ast {
optimize(statements) optimize(statements, &scope)
} else { } else {
statements statements
}, },
@ -1983,7 +2121,7 @@ fn parse_top_level<'a>(
.map(|mut fn_def| { .map(|mut fn_def| {
if optimize_ast { if optimize_ast {
let pos = fn_def.body.position(); 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)); fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos));
} }
Arc::new(fn_def) Arc::new(fn_def)
@ -1994,7 +2132,8 @@ fn parse_top_level<'a>(
pub fn parse<'a>( pub fn parse<'a>(
input: &mut Peekable<TokenIterator<'a>>, input: &mut Peekable<TokenIterator<'a>>,
scope: &Scope,
optimize_ast: bool, optimize_ast: bool,
) -> Result<AST, ParseError> { ) -> Result<AST, ParseError> {
parse_top_level(input, optimize_ast) parse_top_level(input, scope, optimize_ast)
} }

View File

@ -41,6 +41,8 @@ pub enum EvalAltResult {
ErrorVariableNotFound(String, Position), ErrorVariableNotFound(String, Position),
/// Assignment to an inappropriate LHS (left-hand-side) expression. /// Assignment to an inappropriate LHS (left-hand-side) expression.
ErrorAssignmentToUnknownLHS(Position), ErrorAssignmentToUnknownLHS(Position),
/// Assignment to a constant variable.
ErrorAssignmentToConstant(String, Position),
/// Returned type is not the same as the required output type. /// Returned type is not the same as the required output type.
/// Wrapped value is the type of the actual result. /// Wrapped value is the type of the actual result.
ErrorMismatchOutputType(String, Position), ErrorMismatchOutputType(String, Position),
@ -89,6 +91,7 @@ impl Error for EvalAltResult {
Self::ErrorAssignmentToUnknownLHS(_) => { Self::ErrorAssignmentToUnknownLHS(_) => {
"Assignment to an unsupported left-hand side expression" "Assignment to an unsupported left-hand side expression"
} }
Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable",
Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect", Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect",
Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file", Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file",
Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorDotExpr(_, _) => "Malformed dot expression",
@ -116,6 +119,7 @@ impl fmt::Display for EvalAltResult {
Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorFor(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos), Self::ErrorAssignmentToUnknownLHS(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorAssignmentToConstant(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos),
Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos), Self::ErrorMismatchOutputType(s, pos) => write!(f, "{}: {} ({})", desc, s, pos),
Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos), Self::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos),
Self::ErrorDotExpr(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorDotExpr(_, pos) => write!(f, "{} ({})", desc, pos),
@ -213,6 +217,7 @@ impl EvalAltResult {
| Self::ErrorFor(pos) | Self::ErrorFor(pos)
| Self::ErrorVariableNotFound(_, pos) | Self::ErrorVariableNotFound(_, pos)
| Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToUnknownLHS(pos)
| Self::ErrorAssignmentToConstant(_, pos)
| Self::ErrorMismatchOutputType(_, pos) | Self::ErrorMismatchOutputType(_, pos)
| Self::ErrorDotExpr(_, pos) | Self::ErrorDotExpr(_, pos)
| Self::ErrorArithmetic(_, pos) | Self::ErrorArithmetic(_, pos)
@ -238,6 +243,7 @@ impl EvalAltResult {
| Self::ErrorFor(ref mut pos) | Self::ErrorFor(ref mut pos)
| Self::ErrorVariableNotFound(_, ref mut pos) | Self::ErrorVariableNotFound(_, ref mut pos)
| Self::ErrorAssignmentToUnknownLHS(ref mut pos) | Self::ErrorAssignmentToUnknownLHS(ref mut pos)
| Self::ErrorAssignmentToConstant(_, ref mut pos)
| Self::ErrorMismatchOutputType(_, ref mut pos) | Self::ErrorMismatchOutputType(_, ref mut pos)
| Self::ErrorDotExpr(_, ref mut pos) | Self::ErrorDotExpr(_, ref mut pos)
| Self::ErrorArithmetic(_, ref mut pos) | Self::ErrorArithmetic(_, ref mut pos)

View File

@ -1,9 +1,26 @@
//! Module that defines the `Scope` type representing a function call-stack scope. //! Module that defines the `Scope` type representing a function call-stack scope.
use crate::any::{Any, 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; use std::borrow::Cow;
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
pub enum VariableType {
Normal,
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. /// A type containing information about current scope.
/// Useful for keeping state between `Engine` runs. /// Useful for keeping state between `Engine` runs.
/// ///
@ -25,7 +42,7 @@ use std::borrow::Cow;
/// ///
/// When searching for variables, newly-added variables are found before similarly-named but older variables, /// When searching for variables, newly-added variables are found before similarly-named but older variables,
/// allowing for automatic _shadowing_ of variables. /// allowing for automatic _shadowing_ of variables.
pub struct Scope<'a>(Vec<(Cow<'a, str>, Dynamic)>); pub struct Scope<'a>(Vec<ScopeEntry<'a>>);
impl<'a> Scope<'a> { impl<'a> Scope<'a> {
/// Create a new Scope. /// Create a new Scope.
@ -44,18 +61,62 @@ impl<'a> Scope<'a> {
} }
/// Add (push) a new variable to the Scope. /// Add (push) a new variable to the Scope.
pub fn push<K: Into<Cow<'a, str>>, T: Any>(&mut self, key: K, value: T) { pub fn push<K: Into<Cow<'a, str>>, T: Any>(&mut self, name: K, value: T) {
self.0.push((key.into(), Box::new(value))); 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 variable to the Scope. /// Add (push) a new constant to the Scope.
pub(crate) fn push_dynamic<K: Into<Cow<'a, str>>>(&mut self, key: K, value: Dynamic) { pub fn push_constant<K: Into<Cow<'a, str>>, T: Any>(&mut self, name: K, value: T) {
self.0.push((key.into(), value)); 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,
name: K,
var_type: VariableType,
value: Dynamic,
) {
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. /// Remove (pop) the last variable from the Scope.
pub fn pop(&mut self) -> Option<(String, Dynamic)> { pub fn pop(&mut self) -> Option<(String, VariableType, Dynamic)> {
self.0.pop().map(|(key, value)| (key.to_string(), value)) self.0.pop().map(
|ScopeEntry {
name,
var_type,
value,
..
}| (name.to_string(), var_type, value),
)
} }
/// Truncate (rewind) the Scope to a previous size. /// Truncate (rewind) the Scope to a previous size.
@ -64,13 +125,23 @@ impl<'a> Scope<'a> {
} }
/// Find a variable in the Scope, starting from the last. /// Find a variable in the Scope, starting from the last.
pub fn get(&self, key: &str) -> Option<(usize, &str, Dynamic)> { pub fn get(&self, key: &str) -> Option<(usize, &str, VariableType, Dynamic)> {
self.0 self.0
.iter() .iter()
.enumerate() .enumerate()
.rev() // Always search a Scope in reverse order .rev() // Always search a Scope in reverse order
.find(|(_, (name, _))| name == key) .find(|(_, ScopeEntry { name, .. })| name == key)
.map(|(i, (name, value))| (i, name.as_ref(), value.clone())) .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. /// Get the value of a variable in the Scope, starting from the last.
@ -79,53 +150,104 @@ impl<'a> Scope<'a> {
.iter() .iter()
.enumerate() .enumerate()
.rev() // Always search a Scope in reverse order .rev() // Always search a Scope in reverse order
.find(|(_, (name, _))| name == key) .find(|(_, ScopeEntry { name, .. })| name == key)
.and_then(|(_, (_, value))| value.downcast_ref::<T>()) .and_then(|(_, ScopeEntry { value, .. })| value.downcast_ref::<T>())
.map(|value| value.clone()) .map(T::clone)
} }
/// Get a mutable reference to a variable in the Scope. /// 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"); let entry = self.0.get_mut(index).expect("invalid index in Scope");
assert_eq!(entry.0, key, "incorrect key at Scope entry"); assert_ne!(
entry.var_type,
VariableType::Constant,
"get mut of constant variable"
);
assert_eq!(entry.name, name, "incorrect key at Scope entry");
&mut entry.1 &mut entry.value
} }
/// Get a mutable reference to a variable in the Scope and downcast it to a specific type /// Get a mutable reference to a variable in the Scope and downcast it to a specific type
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
pub(crate) fn get_mut_by_type<T: Any + Clone>(&mut self, key: &str, index: usize) -> &mut T { pub(crate) fn get_mut_by_type<T: Any + Clone>(&mut self, name: &str, index: usize) -> &mut T {
self.get_mut(key, index) self.get_mut(name, index)
.downcast_mut::<T>() .downcast_mut::<T>()
.expect("wrong type cast") .expect("wrong type cast")
} }
/// Get an iterator to variables in the Scope. /// Get an iterator to variables in the Scope.
pub fn iter(&self) -> impl Iterator<Item = (&str, &Dynamic)> { pub fn iter(&self) -> impl Iterator<Item = &ScopeEntry> {
self.0 self.0.iter().rev() // Always search a Scope in reverse order
.iter()
.rev() // Always search a Scope in reverse order
.map(|(key, value)| (key.as_ref(), value))
} }
/*
/// 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, Dynamic)> for Scope<'a> impl<'a, K> std::iter::Extend<(K, VariableType, Dynamic)> for Scope<'a>
where where
K: Into<Cow<'a, str>>, K: Into<Cow<'a, str>>,
{ {
fn extend<T: IntoIterator<Item = (K, Dynamic)>>(&mut self, iter: T) { fn extend<T: IntoIterator<Item = (K, VariableType, Dynamic)>>(&mut self, iter: T) {
self.0 self.0
.extend(iter.into_iter().map(|(key, value)| (key.into(), value))); .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)
} }
} }

View File

@ -11,12 +11,12 @@ fn test_chars() -> Result<(), EvalAltResult> {
{ {
assert_eq!(engine.eval::<char>(r#"let x="hello"; x[2]"#)?, 'l'); assert_eq!(engine.eval::<char>(r#"let x="hello"; x[2]"#)?, 'l');
assert_eq!( assert_eq!(
engine.eval::<String>(r#"let x="hello"; x[2]='$'; x"#)?, engine.eval::<String>(r#"let y="hello"; y[2]='$'; y"#)?,
"he$lo".to_string() "he$lo".to_string()
); );
} }
assert!(engine.eval::<char>("'\\uhello'").is_err()); assert!(engine.eval::<char>(r"'\uhello'").is_err());
assert!(engine.eval::<char>("''").is_err()); assert!(engine.eval::<char>("''").is_err());
Ok(()) Ok(())

16
tests/constants.rs Normal file
View File

@ -0,0 +1,16 @@
use rhai::{Engine, EvalAltResult};
#[test]
fn test_constant() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(engine.eval::<i64>("const x = 123; x")?, 123);
match engine.eval::<i64>("const x = 123; x = 42; x") {
Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (),
Err(err) => return Err(err),
Ok(_) => panic!("expecting compilation error"),
}
Ok(())
}