Merge branch 'constants'
This commit is contained in:
commit
cb6171ebcd
25
README.md
25
README.md
@ -57,8 +57,8 @@ Related
|
||||
|
||||
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.
|
||||
* You can also check out the list of [scripting languages for Rust] on [awesome-rust].
|
||||
* [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](https://github.com/rust-unofficial/awesome-rust#scripting) on [awesome-rust](https://github.com/rust-unofficial/awesome-rust)
|
||||
|
||||
Examples
|
||||
--------
|
||||
@ -597,6 +597,17 @@ Variables in Rhai follow normal naming rules (i.e. must contain only ASCII lette
|
||||
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
|
||||
-------
|
||||
|
||||
@ -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
|
||||
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
|
||||
print("done!"); // <-- the line above is further simplified to this
|
||||
// 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/
|
||||
[`debug_msgs`]: #optional-features
|
||||
[`unchecked`]: #optional-features
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
26
src/api.rs
26
src/api.rs
@ -101,8 +101,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(path: PathBuf) -> Result<String, EvalAltResult> {
|
||||
@ -118,8 +124,20 @@ impl<'e> Engine<'e> {
|
||||
|
||||
/// Compile a file into an AST.
|
||||
pub fn compile_file(&self, path: PathBuf) -> Result<AST, EvalAltResult> {
|
||||
Self::read_file(path)
|
||||
.and_then(|contents| self.compile(&contents).map_err(|err| err.into()))
|
||||
self.compile_file_with_scope(&Scope::new(), path)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -235,7 +253,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)
|
||||
|
@ -3,7 +3,7 @@
|
||||
use crate::any::{Any, AnyExt, Dynamic, Variant};
|
||||
use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
|
||||
use crate::result::EvalAltResult;
|
||||
use crate::scope::Scope;
|
||||
use crate::scope::{Scope, VariableType};
|
||||
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
use crate::INT;
|
||||
@ -148,7 +148,8 @@ impl Engine<'_> {
|
||||
fn_def
|
||||
.params
|
||||
.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
|
||||
@ -255,7 +256,7 @@ impl Engine<'_> {
|
||||
}
|
||||
|
||||
// xxx.id
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
|
||||
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) => {
|
||||
let (val, _) = match idx_lhs.as_ref() {
|
||||
// xxx.id[idx_expr]
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
(
|
||||
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
|
||||
@ -294,7 +295,7 @@ impl Engine<'_> {
|
||||
// xxx.dot_lhs.rhs
|
||||
Expr::Dot(dot_lhs, rhs, _) => match dot_lhs.as_ref() {
|
||||
// xxx.id.rhs
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
|
||||
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) => {
|
||||
let (val, _) = match idx_lhs.as_ref() {
|
||||
// xxx.id[idx_expr].rhs
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
(
|
||||
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)?,
|
||||
@ -353,8 +354,8 @@ impl Engine<'_> {
|
||||
) -> Result<Dynamic, EvalAltResult> {
|
||||
match dot_lhs {
|
||||
// id.???
|
||||
Expr::Identifier(id, pos) => {
|
||||
let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
|
||||
Expr::Variable(id, 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);
|
||||
|
||||
// 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,
|
||||
map: impl FnOnce(Dynamic) -> Result<T, EvalAltResult>,
|
||||
begin: Position,
|
||||
) -> Result<(usize, T), EvalAltResult> {
|
||||
) -> Result<(usize, VariableType, T), EvalAltResult> {
|
||||
scope
|
||||
.get(id)
|
||||
.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)
|
||||
@ -481,13 +482,13 @@ impl Engine<'_> {
|
||||
|
||||
match lhs {
|
||||
// id[idx_expr]
|
||||
Expr::Identifier(id, _) => Self::search_scope(
|
||||
Expr::Variable(id, _) => Self::search_scope(
|
||||
scope,
|
||||
&id,
|
||||
|val| self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos),
|
||||
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)
|
||||
}),
|
||||
|
||||
@ -585,7 +586,7 @@ impl Engine<'_> {
|
||||
) -> Result<Dynamic, EvalAltResult> {
|
||||
match dot_rhs {
|
||||
// xxx.id
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let set_fn_name = format!("{}{}", FUNC_SETTER, id);
|
||||
|
||||
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"))]
|
||||
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
|
||||
// xxx.id[idx_expr]
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
|
||||
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
|
||||
@ -620,7 +621,7 @@ impl Engine<'_> {
|
||||
// xxx.lhs.{...}
|
||||
Expr::Dot(lhs, rhs, _) => match lhs.as_ref() {
|
||||
// xxx.id.rhs
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
|
||||
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
|
||||
@ -640,7 +641,7 @@ impl Engine<'_> {
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Expr::Index(lhs, idx_expr, idx_pos) => match lhs.as_ref() {
|
||||
// xxx.id[idx_expr].rhs
|
||||
Expr::Identifier(id, pos) => {
|
||||
Expr::Property(id, pos) => {
|
||||
let get_fn_name = format!("{}{}", FUNC_GETTER, id);
|
||||
|
||||
self.call_fn_raw(&get_fn_name, vec![this_ptr], None, *pos)
|
||||
@ -702,11 +703,23 @@ impl Engine<'_> {
|
||||
dot_rhs: &Expr,
|
||||
new_val: Dynamic,
|
||||
val_pos: Position,
|
||||
op_pos: Position,
|
||||
) -> Result<Dynamic, EvalAltResult> {
|
||||
match dot_lhs {
|
||||
// id.???
|
||||
Expr::Identifier(id, pos) => {
|
||||
let (src_idx, mut target) = Self::search_scope(scope, id, Ok, *pos)?;
|
||||
Expr::Variable(id, 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 =
|
||||
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::StringConstant(s, _) => Ok(s.into_dynamic()),
|
||||
Expr::CharConstant(c, _) => Ok(c.into_dynamic()),
|
||||
Expr::Identifier(id, pos) => {
|
||||
Self::search_scope(scope, id, Ok, *pos).map(|(_, val)| val)
|
||||
Expr::Variable(id, pos) => {
|
||||
Self::search_scope(scope, id, Ok, *pos).map(|(_, _, val)| val)
|
||||
}
|
||||
Expr::Property(_, _) => panic!("unexpected property."),
|
||||
|
||||
// lhs[idx_expr]
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
@ -775,19 +789,21 @@ impl Engine<'_> {
|
||||
Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt),
|
||||
|
||||
// lhs = rhs
|
||||
Expr::Assignment(lhs, rhs, _) => {
|
||||
Expr::Assignment(lhs, rhs, op_pos) => {
|
||||
let rhs_val = self.eval_expr(scope, rhs)?;
|
||||
|
||||
match lhs.as_ref() {
|
||||
// name = rhs
|
||||
Expr::Identifier(name, pos) => {
|
||||
if let Some((idx, _, _)) = scope.get(name) {
|
||||
Expr::Variable(name, pos) => match scope.get(name) {
|
||||
Some((idx, _, VariableType::Normal, _)) => {
|
||||
*scope.get_mut(name, idx) = rhs_val;
|
||||
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
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
@ -814,9 +830,15 @@ impl Engine<'_> {
|
||||
|
||||
// dot_lhs.dot_rhs = 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
|
||||
_ => Err(EvalAltResult::ErrorAssignmentToUnknownLHS(lhs.position())),
|
||||
}
|
||||
@ -1045,7 +1067,7 @@ impl Engine<'_> {
|
||||
// Let statement
|
||||
Stmt::Let(name, Some(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())
|
||||
}
|
||||
|
||||
@ -1053,6 +1075,15 @@ impl Engine<'_> {
|
||||
scope.push(name.clone(), ());
|
||||
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!"),
|
||||
}
|
||||
}
|
||||
|
||||
|
20
src/error.rs
20
src/error.rs
@ -67,6 +67,8 @@ pub enum ParseErrorType {
|
||||
MalformedCallExpr(String),
|
||||
/// An expression in indexing brackets `[]` has syntax error.
|
||||
MalformedIndexExpr(String),
|
||||
/// Invalid expression assigned to constant.
|
||||
ForbiddenConstantExpr(String),
|
||||
/// Missing a variable name after the `let` keyword.
|
||||
VarExpectsIdentifier,
|
||||
/// Defining a function `fn` in an appropriate place (e.g. inside another function).
|
||||
@ -77,6 +79,10 @@ pub enum ParseErrorType {
|
||||
FnMissingParams(String),
|
||||
/// Assignment to an inappropriate LHS (left-hand-side) expression.
|
||||
AssignmentToInvalidLHS,
|
||||
/// Assignment to a copy of a value.
|
||||
AssignmentToCopy,
|
||||
/// Assignment to an a constant variable.
|
||||
AssignmentToConstant(String),
|
||||
}
|
||||
|
||||
/// Error when parsing a script.
|
||||
@ -112,11 +118,14 @@ impl Error for ParseError {
|
||||
ParseErrorType::MissingRightBracket(_) => "Expecting ']'",
|
||||
ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments",
|
||||
ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression",
|
||||
ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant",
|
||||
ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable",
|
||||
ParseErrorType::FnMissingName => "Expecting name 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::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) => {
|
||||
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::FnMissingParams(ref s) => {
|
||||
write!(f, "Expecting parameters for function '{}'", s)?
|
||||
@ -142,6 +154,12 @@ impl fmt::Display for ParseError {
|
||||
| ParseErrorType::MissingRightBracket(ref 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())?,
|
||||
}
|
||||
|
||||
|
220
src/optimize.rs
220
src/optimize.rs
@ -1,13 +1,52 @@
|
||||
use crate::engine::KEYWORD_DUMP_AST;
|
||||
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 {
|
||||
Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
|
||||
let pos = expr.position();
|
||||
let expr = optimize_expr(*expr, changed);
|
||||
let expr = optimize_expr(*expr, state);
|
||||
|
||||
match expr {
|
||||
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 {
|
||||
Expr::False(pos) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
Stmt::Noop(pos)
|
||||
}
|
||||
Expr::True(_) => optimize_stmt(*stmt1, changed, true),
|
||||
Expr::True(_) => optimize_stmt(*stmt1, state, true),
|
||||
expr => Stmt::IfElse(
|
||||
Box::new(optimize_expr(expr, changed)),
|
||||
Box::new(optimize_stmt(*stmt1, changed, true)),
|
||||
Box::new(optimize_expr(expr, state)),
|
||||
Box::new(optimize_stmt(*stmt1, state, true)),
|
||||
None,
|
||||
),
|
||||
},
|
||||
|
||||
Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr {
|
||||
Expr::False(_) => optimize_stmt(*stmt2, changed, true),
|
||||
Expr::True(_) => optimize_stmt(*stmt1, changed, true),
|
||||
Expr::False(_) => optimize_stmt(*stmt2, state, true),
|
||||
Expr::True(_) => optimize_stmt(*stmt1, state, true),
|
||||
expr => Stmt::IfElse(
|
||||
Box::new(optimize_expr(expr, changed)),
|
||||
Box::new(optimize_stmt(*stmt1, changed, true)),
|
||||
match optimize_stmt(*stmt2, changed, true) {
|
||||
Box::new(optimize_expr(expr, state)),
|
||||
Box::new(optimize_stmt(*stmt1, state, true)),
|
||||
match optimize_stmt(*stmt2, state, true) {
|
||||
stmt if stmt.is_noop() => None,
|
||||
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 {
|
||||
Expr::False(pos) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
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(
|
||||
Box::new(optimize_expr(expr, changed)),
|
||||
Box::new(optimize_stmt(*stmt, changed, false)),
|
||||
Box::new(optimize_expr(expr, state)),
|
||||
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(
|
||||
id,
|
||||
Box::new(optimize_expr(*expr, changed)),
|
||||
Box::new(optimize_stmt(*stmt, changed, false)),
|
||||
Box::new(optimize_expr(*expr, state)),
|
||||
Box::new(optimize_stmt(*stmt, state, false)),
|
||||
),
|
||||
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::Block(statements, pos) => {
|
||||
let orig_len = statements.len();
|
||||
let orig_constants = state.constants.len();
|
||||
|
||||
let mut result: Vec<_> = statements
|
||||
.into_iter() // For each statement
|
||||
.rev() // Scan in reverse
|
||||
.map(|s| optimize_stmt(s, changed, preserve_result)) // Optimize the statement
|
||||
.map(|stmt| {
|
||||
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()
|
||||
.filter(|(i, s)| s.is_op() || (preserve_result && *i == 0)) // Remove no-op's but leave the last one if we need the result
|
||||
.map(|(_, s)| s)
|
||||
.rev()
|
||||
.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(|(_, stmt)| stmt)
|
||||
.collect();
|
||||
|
||||
// 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()
|
||||
.rev()
|
||||
.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()
|
||||
.collect();
|
||||
}
|
||||
|
||||
*changed = *changed || orig_len != result.len();
|
||||
if orig_len != result.len() {
|
||||
state.set_dirty();
|
||||
}
|
||||
|
||||
state.restore_constants(orig_constants);
|
||||
|
||||
match result[..] {
|
||||
// No statements in block - change to No-op
|
||||
[] => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
Stmt::Noop(pos)
|
||||
}
|
||||
// Only one statement - promote
|
||||
[_] => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
result.remove(0)
|
||||
}
|
||||
_ => 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(
|
||||
Some(Box::new(optimize_expr(*expr, changed))),
|
||||
is_return,
|
||||
pos,
|
||||
),
|
||||
Stmt::ReturnWithVal(Some(expr), is_return, pos) => {
|
||||
Stmt::ReturnWithVal(Some(Box::new(optimize_expr(*expr, state))), is_return, pos)
|
||||
}
|
||||
|
||||
stmt => stmt,
|
||||
}
|
||||
}
|
||||
|
||||
fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
|
||||
fn optimize_expr(expr: Expr, state: &mut State) -> 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(_) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
Expr::Unit(pos)
|
||||
}
|
||||
Stmt::Expr(expr) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
*expr
|
||||
}
|
||||
stmt => Expr::Stmt(Box::new(stmt), 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(
|
||||
Box::new(optimize_expr(*lhs, changed)),
|
||||
Box::new(optimize_expr(*rhs, changed)),
|
||||
Box::new(optimize_expr(*lhs, state)),
|
||||
Box::new(optimize_expr(*rhs, state)),
|
||||
pos,
|
||||
),
|
||||
|
||||
@ -186,12 +234,12 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
|
||||
{
|
||||
// Array where everything is a pure - promote the indexed item.
|
||||
// All other items can be thrown away.
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
items.remove(i as usize)
|
||||
}
|
||||
(lhs, rhs) => Expr::Index(
|
||||
Box::new(optimize_expr(lhs, changed)),
|
||||
Box::new(optimize_expr(rhs, changed)),
|
||||
Box::new(optimize_expr(lhs, state)),
|
||||
Box::new(optimize_expr(rhs, state)),
|
||||
pos,
|
||||
),
|
||||
},
|
||||
@ -204,10 +252,12 @@ fn optimize_expr(expr: Expr, changed: &mut bool) -> Expr {
|
||||
|
||||
let items: Vec<_> = items
|
||||
.into_iter()
|
||||
.map(|expr| optimize_expr(expr, changed))
|
||||
.map(|expr| optimize_expr(expr, state))
|
||||
.collect();
|
||||
|
||||
*changed = *changed || orig_len != items.len();
|
||||
if orig_len != items.len() {
|
||||
state.set_dirty();
|
||||
}
|
||||
|
||||
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::True(_), rhs) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
rhs
|
||||
}
|
||||
(Expr::False(pos), _) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
Expr::False(pos)
|
||||
}
|
||||
(lhs, Expr::True(_)) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
lhs
|
||||
}
|
||||
(lhs, rhs) => Expr::And(
|
||||
Box::new(optimize_expr(lhs, changed)),
|
||||
Box::new(optimize_expr(rhs, changed)),
|
||||
Box::new(optimize_expr(lhs, state)),
|
||||
Box::new(optimize_expr(rhs, state)),
|
||||
),
|
||||
},
|
||||
Expr::Or(lhs, rhs) => match (*lhs, *rhs) {
|
||||
(Expr::False(_), rhs) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
rhs
|
||||
}
|
||||
(Expr::True(pos), _) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
Expr::True(pos)
|
||||
}
|
||||
(lhs, Expr::False(_)) => {
|
||||
*changed = true;
|
||||
state.set_dirty();
|
||||
lhs
|
||||
}
|
||||
(lhs, rhs) => Expr::Or(
|
||||
Box::new(optimize_expr(lhs, changed)),
|
||||
Box::new(optimize_expr(rhs, changed)),
|
||||
Box::new(optimize_expr(lhs, state)),
|
||||
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) => {
|
||||
let orig_len = args.len();
|
||||
|
||||
let args: Vec<_> = args
|
||||
.into_iter()
|
||||
.map(|a| optimize_expr(a, changed))
|
||||
.collect();
|
||||
let args: Vec<_> = args.into_iter().map(|a| optimize_expr(a, state)).collect();
|
||||
|
||||
*changed = *changed || orig_len != args.len();
|
||||
if orig_len != args.len() {
|
||||
state.set_dirty();
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
.into_iter()
|
||||
.rev() // Scan in reverse
|
||||
.enumerate()
|
||||
.map(|(i, stmt)| {
|
||||
// Keep all variable declarations at this level
|
||||
let keep = stmt.is_var();
|
||||
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
|
||||
// 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 changed, keep || i == 0)
|
||||
optimize_stmt(stmt, &mut state, keep)
|
||||
}
|
||||
})
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
if !changed {
|
||||
if !state.is_dirty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
195
src/parser.rs
195
src/parser.rs
@ -3,6 +3,7 @@
|
||||
use crate::any::Dynamic;
|
||||
use crate::error::{LexError, ParseError, ParseErrorType};
|
||||
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,
|
||||
@ -179,6 +216,7 @@ pub enum Stmt {
|
||||
Loop(Box<Stmt>),
|
||||
For(String, Box<Expr>, Box<Stmt>),
|
||||
Let(String, Option<Box<Expr>>, Position),
|
||||
Const(String, Box<Expr>, Position),
|
||||
Block(Vec<Stmt>, Position),
|
||||
Expr(Box<Expr>),
|
||||
Break(Position),
|
||||
@ -211,6 +249,7 @@ impl Stmt {
|
||||
match self {
|
||||
Stmt::Noop(pos)
|
||||
| Stmt::Let(_, _, pos)
|
||||
| Stmt::Const(_, _, pos)
|
||||
| Stmt::Block(_, pos)
|
||||
| Stmt::Break(pos)
|
||||
| Stmt::ReturnWithVal(_, _, pos) => *pos,
|
||||
@ -225,7 +264,8 @@ pub enum Expr {
|
||||
IntegerConstant(INT, Position),
|
||||
#[cfg(not(feature = "no_float"))]
|
||||
FloatConstant(FLOAT, Position),
|
||||
Identifier(String, Position),
|
||||
Variable(String, Position),
|
||||
Property(String, Position),
|
||||
CharConstant(char, Position),
|
||||
StringConstant(String, Position),
|
||||
Stmt(Box<Stmt>, Position),
|
||||
@ -242,12 +282,29 @@ pub enum 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 {
|
||||
match self {
|
||||
Expr::IntegerConstant(_, pos)
|
||||
| Expr::Identifier(_, pos)
|
||||
| Expr::CharConstant(_, pos)
|
||||
| Expr::StringConstant(_, pos)
|
||||
| Expr::Variable(_, pos)
|
||||
| Expr::Property(_, pos)
|
||||
| Expr::Stmt(_, pos)
|
||||
| Expr::FunctionCall(_, _, _, pos)
|
||||
| Expr::Array(_, pos)
|
||||
@ -273,7 +330,7 @@ impl Expr {
|
||||
match self {
|
||||
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 => expr.is_constant() || expr.is_identifier(),
|
||||
expr => expr.is_constant() || expr.is_variable(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,9 +349,17 @@ impl Expr {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
pub fn is_identifier(&self) -> bool {
|
||||
|
||||
pub fn is_variable(&self) -> bool {
|
||||
match self {
|
||||
Expr::Identifier(_, _) => true,
|
||||
Expr::Variable(_, _) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_property(&self) -> bool {
|
||||
match self {
|
||||
Expr::Property(_, _) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -328,6 +393,7 @@ pub enum Token {
|
||||
True,
|
||||
False,
|
||||
Let,
|
||||
Const,
|
||||
If,
|
||||
Else,
|
||||
While,
|
||||
@ -402,6 +468,7 @@ impl Token {
|
||||
True => "true",
|
||||
False => "false",
|
||||
Let => "let",
|
||||
Const => "const",
|
||||
If => "if",
|
||||
Else => "else",
|
||||
While => "while",
|
||||
@ -829,6 +896,7 @@ impl<'a> TokenIterator<'a> {
|
||||
"true" => Token::True,
|
||||
"false" => Token::False,
|
||||
"let" => Token::Let,
|
||||
"const" => Token::Const,
|
||||
"if" => Token::If,
|
||||
"else" => Token::Else,
|
||||
"while" => Token::While,
|
||||
@ -1360,10 +1428,10 @@ fn parse_ident_expr<'a>(
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Some(&(Token::LeftBracket, pos)) => {
|
||||
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)),
|
||||
None => Ok(Expr::Identifier(id, Position::eof())),
|
||||
Some(_) => Ok(Expr::Variable(id, begin)),
|
||||
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 valid_assignment_chain(expr: &Expr) -> (bool, Position) {
|
||||
fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option<ParseError> {
|
||||
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"))]
|
||||
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"))]
|
||||
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::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"))]
|
||||
Expr::Index(idx_lhs, _, _) if idx_lhs.is_identifier() => {
|
||||
valid_assignment_chain(dot_rhs)
|
||||
Expr::Index(idx_lhs, _, _)
|
||||
if (idx_lhs.is_variable() && is_top) || (idx_lhs.is_property() && !is_top) =>
|
||||
{
|
||||
valid_assignment_chain(dot_rhs, false)
|
||||
}
|
||||
#[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);
|
||||
|
||||
match valid_assignment_chain(&lhs) {
|
||||
(true, _) => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)),
|
||||
(false, pos) => Err(ParseError::new(PERR::AssignmentToInvalidLHS, pos)),
|
||||
match valid_assignment_chain(&lhs, true) {
|
||||
None => Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)),
|
||||
Some(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1619,7 +1719,28 @@ fn parse_binary_op<'a>(
|
||||
Token::PlusAssign => 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
|
||||
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)))
|
||||
}
|
||||
|
||||
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() {
|
||||
Some((_, tok_pos)) => tok_pos,
|
||||
_ => 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, _)) => {
|
||||
input.next();
|
||||
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)),
|
||||
}
|
||||
@ -1867,7 +2003,8 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
|
||||
}
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
@ -1943,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();
|
||||
@ -1973,7 +2111,7 @@ fn parse_top_level<'a>(
|
||||
|
||||
return Ok(AST(
|
||||
if optimize_ast {
|
||||
optimize(statements)
|
||||
optimize(statements, &scope)
|
||||
} else {
|
||||
statements
|
||||
},
|
||||
@ -1983,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)
|
||||
@ -1994,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)
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ pub enum EvalAltResult {
|
||||
ErrorVariableNotFound(String, Position),
|
||||
/// Assignment to an inappropriate LHS (left-hand-side) expression.
|
||||
ErrorAssignmentToUnknownLHS(Position),
|
||||
/// Assignment to a constant variable.
|
||||
ErrorAssignmentToConstant(String, Position),
|
||||
/// Returned type is not the same as the required output type.
|
||||
/// Wrapped value is the type of the actual result.
|
||||
ErrorMismatchOutputType(String, Position),
|
||||
@ -89,6 +91,7 @@ impl Error for EvalAltResult {
|
||||
Self::ErrorAssignmentToUnknownLHS(_) => {
|
||||
"Assignment to an unsupported left-hand side expression"
|
||||
}
|
||||
Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable",
|
||||
Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect",
|
||||
Self::ErrorReadingScriptFile(_, _) => "Cannot read from script file",
|
||||
Self::ErrorDotExpr(_, _) => "Malformed dot expression",
|
||||
@ -116,6 +119,7 @@ impl fmt::Display for EvalAltResult {
|
||||
Self::ErrorIfGuard(pos) => write!(f, "{} ({})", desc, pos),
|
||||
Self::ErrorFor(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::ErrorDotExpr(s, pos) if !s.is_empty() => write!(f, "{} {} ({})", desc, s, pos),
|
||||
Self::ErrorDotExpr(_, pos) => write!(f, "{} ({})", desc, pos),
|
||||
@ -213,6 +217,7 @@ impl EvalAltResult {
|
||||
| Self::ErrorFor(pos)
|
||||
| Self::ErrorVariableNotFound(_, pos)
|
||||
| Self::ErrorAssignmentToUnknownLHS(pos)
|
||||
| Self::ErrorAssignmentToConstant(_, pos)
|
||||
| Self::ErrorMismatchOutputType(_, pos)
|
||||
| Self::ErrorDotExpr(_, pos)
|
||||
| Self::ErrorArithmetic(_, pos)
|
||||
@ -238,6 +243,7 @@ impl EvalAltResult {
|
||||
| Self::ErrorFor(ref mut pos)
|
||||
| Self::ErrorVariableNotFound(_, ref mut pos)
|
||||
| Self::ErrorAssignmentToUnknownLHS(ref mut pos)
|
||||
| Self::ErrorAssignmentToConstant(_, ref mut pos)
|
||||
| Self::ErrorMismatchOutputType(_, ref mut pos)
|
||||
| Self::ErrorDotExpr(_, ref mut pos)
|
||||
| Self::ErrorArithmetic(_, ref mut pos)
|
||||
|
198
src/scope.rs
198
src/scope.rs
@ -1,9 +1,26 @@
|
||||
//! 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;
|
||||
|
||||
#[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.
|
||||
/// 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,
|
||||
/// 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> {
|
||||
/// Create a new Scope.
|
||||
@ -44,18 +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(), 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 variable to the Scope.
|
||||
pub(crate) fn push_dynamic<K: Into<Cow<'a, str>>>(&mut self, key: K, value: Dynamic) {
|
||||
self.0.push((key.into(), value));
|
||||
/// Add (push) a new constant to the Scope.
|
||||
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,
|
||||
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.
|
||||
pub fn pop(&mut self) -> Option<(String, Dynamic)> {
|
||||
self.0.pop().map(|(key, value)| (key.to_string(), value))
|
||||
pub fn pop(&mut self) -> Option<(String, VariableType, Dynamic)> {
|
||||
self.0.pop().map(
|
||||
|ScopeEntry {
|
||||
name,
|
||||
var_type,
|
||||
value,
|
||||
..
|
||||
}| (name.to_string(), var_type, value),
|
||||
)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn get(&self, key: &str) -> Option<(usize, &str, Dynamic)> {
|
||||
pub fn get(&self, key: &str) -> Option<(usize, &str, VariableType, Dynamic)> {
|
||||
self.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev() // Always search a Scope in reverse order
|
||||
.find(|(_, (name, _))| name == key)
|
||||
.map(|(i, (name, value))| (i, name.as_ref(), 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.
|
||||
@ -79,53 +150,104 @@ 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_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
|
||||
#[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, &Dynamic)> {
|
||||
self.0
|
||||
.iter()
|
||||
.rev() // Always search a Scope in reverse order
|
||||
.map(|(key, value)| (key.as_ref(), 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, Dynamic)> for Scope<'a>
|
||||
impl<'a, K> std::iter::Extend<(K, VariableType, Dynamic)> for Scope<'a>
|
||||
where
|
||||
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
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -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::<String>(r#"let x="hello"; x[2]='$'; x"#)?,
|
||||
engine.eval::<String>(r#"let y="hello"; y[2]='$'; y"#)?,
|
||||
"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());
|
||||
|
||||
Ok(())
|
||||
|
16
tests/constants.rs
Normal file
16
tests/constants.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user