diff --git a/README.md b/README.md index 57f3c463..6cbb3280 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/examples/repl.rs b/examples/repl.rs index 381bc265..7e32ec16 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -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); diff --git a/scripts/primes.rhai b/scripts/primes.rhai index fae57bbb..d3892bce 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -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); diff --git a/src/api.rs b/src/api.rs index c12d86bf..ba6f8bf4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -101,8 +101,14 @@ impl<'e> Engine<'e> { /// Compile a string into an AST. pub fn compile(&self, input: &str) -> Result { + 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 { 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 { @@ -118,8 +124,20 @@ impl<'e> Engine<'e> { /// Compile a file into an AST. pub fn compile_file(&self, path: PathBuf) -> Result { - 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 { + 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) 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..95f36186 100644 --- a/src/optimize.rs +++ b/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) -> Vec { +pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { 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; } } diff --git a/src/parser.rs b/src/parser.rs index a02baf0f..89021d39 100644 --- a/src/parser.rs +++ b/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>, ); +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), For(String, Box, Box), Let(String, Option>, Position), + Const(String, Box, Position), Block(Vec, Position), Expr(Box), 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, 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>) -> 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), } } @@ -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>) -> 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())), @@ -1779,7 +1903,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)), } @@ -1867,7 +2003,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), } } @@ -1943,6 +2080,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( input: &mut Peekable>, + scope: &Scope, optimize_ast: bool, ) -> Result { let mut statements = Vec::::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>, + scope: &Scope, optimize_ast: bool, ) -> Result { - parse_top_level(input, optimize_ast) + parse_top_level(input, scope, optimize_ast) } diff --git a/src/result.rs b/src/result.rs index c37a7a4a..acafecc4 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..e41b7b67 100644 --- a/src/scope.rs +++ b/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, +} + /// 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>); 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>, T: Any>(&mut self, key: K, value: T) { - self.0.push((key.into(), Box::new(value))); + pub fn push>, 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>>(&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, 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>>( + &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::()) - .map(|value| value.clone()) + .find(|(_, ScopeEntry { name, .. })| name == key) + .and_then(|(_, ScopeEntry { value, .. })| value.downcast_ref::()) + .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(&mut self, key: &str, index: usize) -> &mut T { - self.get_mut(key, index) + pub(crate) fn get_mut_by_type(&mut self, name: &str, index: usize) -> &mut T { + self.get_mut(name, index) .downcast_mut::() .expect("wrong type cast") } /// Get an iterator to variables in the Scope. - pub fn iter(&self) -> impl Iterator { - self.0 - .iter() - .rev() // Always search a Scope in reverse order - .map(|(key, value)| (key.as_ref(), value)) + pub fn iter(&self) -> impl Iterator { + 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 { - 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>, { - fn extend>(&mut self, iter: T) { + fn extend>(&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, Dynamic) { + if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::IntegerConstant( + *value.downcast::().expect("value should be INT"), + Position::none(), + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::FloatConstant( + *value.downcast::().expect("value should be FLOAT"), + Position::none(), + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::CharConstant( + *value.downcast::().expect("value should be char"), + Position::none(), + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::StringConstant( + *value.downcast::().expect("value should be String"), + Position::none(), + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some( + if *value.downcast::().expect("value should be bool") { + Expr::True(Position::none()) + } else { + Expr::False(Position::none()) + }, + ), + value2, + ) + } else { + (None, 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(()) +}