diff --git a/README.md b/README.md index 6ce0ad15..d265d8aa 100644 --- a/README.md +++ b/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 diff --git a/src/engine.rs b/src/engine.rs index f3fa3ad4..af3f4606 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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 { 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, 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 { 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 { 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!"), } } diff --git a/src/error.rs b/src/error.rs index 8733682b..e769d993 100644 --- a/src/error.rs +++ b/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())?, } diff --git a/src/optimize.rs b/src/optimize.rs index bf7eab8f..102839d9 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -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) -> Vec { 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)| { - // 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; } } diff --git a/src/parser.rs b/src/parser.rs index 66eec95f..0b0e0018 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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), For(String, Box, Box), Let(String, Option>, Position), + Const(String, Box, Position), Block(Vec, Position), Expr(Box), 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, 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>) -> Result Result { - fn valid_assignment_chain(expr: &Expr) -> (bool, Position) { + fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option { 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>) -> Result(input: &mut Peekable>) -> Result { +fn parse_var<'a>( + input: &mut Peekable>, + var_type: VariableType, +) -> Result { 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>) -> Result { 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>) -> Result 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), } } diff --git a/src/result.rs b/src/result.rs index e0bb3734..ca9438b8 100644 --- a/src/result.rs +++ b/src/result.rs @@ -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) diff --git a/src/scope.rs b/src/scope.rs index cf726d39..b3eef176 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -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>, 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>>(&mut self, key: K, value: Dynamic) { - self.0.push((key.into(), value)); + /// Add (push) a new constant to the Scope. + pub fn push_constant>, T: Any>(&mut self, key: K, value: T) { + self.0 + .push((key.into(), VariableType::Constant, Box::new(value))); + } + + /// Add (push) a new variable with a `Dynamic` value to the Scope. + pub(crate) fn push_dynamic>>( + &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::()) + .find(|(_, (name, _, _))| name == key) + .and_then(|(_, (_, _, value))| value.downcast_ref::()) .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 { + pub fn iter(&self) -> impl Iterator { 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>, { - fn extend>(&mut self, iter: T) { - self.0 - .extend(iter.into_iter().map(|(key, value)| (key.into(), value))); + fn extend>(&mut self, iter: T) { + self.0.extend( + iter.into_iter() + .map(|(key, var_type, value)| (key.into(), var_type, value)), + ); } } diff --git a/tests/chars.rs b/tests/chars.rs index cf207e43..340cc392 100644 --- a/tests/chars.rs +++ b/tests/chars.rs @@ -11,12 +11,12 @@ fn test_chars() -> Result<(), EvalAltResult> { { assert_eq!(engine.eval::(r#"let x="hello"; x[2]"#)?, 'l'); assert_eq!( - engine.eval::(r#"let x="hello"; x[2]='$'; x"#)?, + engine.eval::(r#"let y="hello"; y[2]='$'; y"#)?, "he$lo".to_string() ); } - assert!(engine.eval::("'\\uhello'").is_err()); + assert!(engine.eval::(r"'\uhello'").is_err()); assert!(engine.eval::("''").is_err()); Ok(()) diff --git a/tests/constants.rs b/tests/constants.rs new file mode 100644 index 00000000..e81f7ce4 --- /dev/null +++ b/tests/constants.rs @@ -0,0 +1,16 @@ +use rhai::{Engine, EvalAltResult}; + +#[test] +fn test_constant() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + assert_eq!(engine.eval::("const x = 123; x")?, 123); + + match engine.eval::("const x = 123; x = 42; x") { + Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (), + Err(err) => return Err(err), + Ok(_) => panic!("expecting compilation error"), + } + + Ok(()) +}