Add constants.

This commit is contained in:
Stephen Chung 2020-03-13 18:12:41 +08:00
parent 9bd66c7db3
commit 9844ae8665
9 changed files with 426 additions and 155 deletions

View File

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

View File

@ -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!"),
}
}

View File

@ -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())?,
}

View File

@ -1,13 +1,51 @@
use crate::engine::KEYWORD_DUMP_AST;
use crate::parser::{Expr, Stmt};
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 +63,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 +89,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 +168,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 +233,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 +251,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 +266,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,16 +307,25 @@ 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,
}
}
@ -276,23 +334,28 @@ pub(crate) fn optimize(statements: Vec<Stmt>) -> Vec<Stmt> {
let mut result = statements;
loop {
let mut changed = false;
let mut state = State::new();
let num_statements = result.len();
result = result
.into_iter()
.rev() // Scan in reverse
.enumerate()
.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
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 changed, keep || i == 0)
optimize_stmt(stmt, &mut state, keep)
}
})
.rev()
.collect();
if !changed {
if !state.is_dirty() {
break;
}
}

View File

@ -2,7 +2,7 @@
use crate::any::Dynamic;
use crate::error::{LexError, ParseError, ParseErrorType};
use crate::optimize::optimize;
use crate::{optimize::optimize, scope::VariableType};
use std::{
borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc,
@ -179,6 +179,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 +212,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 +227,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 +245,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 +293,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 +312,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 +356,7 @@ pub enum Token {
True,
False,
Let,
Const,
If,
Else,
While,
@ -402,6 +431,7 @@ impl Token {
True => "true",
False => "false",
Let => "let",
Const => "const",
If => "if",
Else => "else",
While => "while",
@ -829,6 +859,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,
@ -891,7 +922,8 @@ impl<'a> TokenIterator<'a> {
}
'-' => match self.char_stream.peek() {
// Negative number?
Some('0'..='9') => negated = true,
Some('0'..='9') if self.last.is_next_unary() => negated = true,
Some('0'..='9') => return Some((Token::Minus, pos)),
Some('=') => {
self.char_stream.next();
self.advance();
@ -1359,10 +1391,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())),
}
}
@ -1521,37 +1553,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),
}
}
@ -1618,7 +1682,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(
@ -1762,7 +1847,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())),
@ -1778,7 +1866,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)),
}
@ -1866,7 +1966,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),
}
}

View File

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

View File

@ -4,6 +4,12 @@ use crate::any::{Any, Dynamic};
use std::borrow::Cow;
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
pub enum VariableType {
Normal,
Constant,
}
/// A type containing information about current scope.
/// Useful for keeping state between `Engine` runs.
///
@ -25,7 +31,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<(Cow<'a, str>, VariableType, Dynamic)>);
impl<'a> Scope<'a> {
/// Create a new Scope.
@ -45,17 +51,31 @@ 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)));
self.0
.push((key.into(), VariableType::Normal, Box::new(value)));
}
/// 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, key: K, value: T) {
self.0
.push((key.into(), VariableType::Constant, Box::new(value)));
}
/// Add (push) a new variable with a `Dynamic` value to the Scope.
pub(crate) fn push_dynamic<K: Into<Cow<'a, str>>>(
&mut self,
key: K,
val_type: VariableType,
value: Dynamic,
) {
self.0.push((key.into(), val_type, value));
}
/// 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(|(key, var_type, value)| (key.to_string(), var_type, value))
}
/// Truncate (rewind) the Scope to a previous size.
@ -64,13 +84,13 @@ 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(|(_, (name, _, _))| name == key)
.map(|(i, (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,8 +99,8 @@ 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>())
.find(|(_, (name, _, _))| name == key)
.and_then(|(_, (_, _, value))| value.downcast_ref::<T>())
.map(|value| value.clone())
}
@ -88,9 +108,14 @@ impl<'a> Scope<'a> {
pub(crate) fn get_mut(&mut self, key: &str, index: usize) -> &mut Dynamic {
let entry = self.0.get_mut(index).expect("invalid index in Scope");
assert_ne!(
entry.1,
VariableType::Constant,
"get mut of constant variable"
);
assert_eq!(entry.0, key, "incorrect key at Scope entry");
&mut entry.1
&mut entry.2
}
/// Get a mutable reference to a variable in the Scope and downcast it to a specific type
@ -102,11 +127,11 @@ impl<'a> Scope<'a> {
}
/// Get an iterator to variables in the Scope.
pub fn iter(&self) -> impl Iterator<Item = (&str, &Dynamic)> {
pub fn iter(&self) -> impl Iterator<Item = (&str, VariableType, &Dynamic)> {
self.0
.iter()
.rev() // Always search a Scope in reverse order
.map(|(key, value)| (key.as_ref(), value))
.map(|(key, var_type, value)| (key.as_ref(), *var_type, value))
}
/*
@ -120,12 +145,14 @@ impl<'a> Scope<'a> {
*/
}
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) {
self.0
.extend(iter.into_iter().map(|(key, value)| (key.into(), value)));
fn extend<T: IntoIterator<Item = (K, VariableType, Dynamic)>>(&mut self, iter: T) {
self.0.extend(
iter.into_iter()
.map(|(key, var_type, value)| (key.into(), var_type, value)),
);
}
}

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::<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
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(())
}