From 9844ae86652897f3ee3092e21a167c0ed73ca4b4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 13 Mar 2020 18:12:41 +0800 Subject: [PATCH 01/21] Add constants. --- README.md | 25 ++++-- src/engine.rs | 87 ++++++++++++------- src/error.rs | 20 ++++- src/optimize.rs | 203 +++++++++++++++++++++++++++++---------------- src/parser.rs | 155 ++++++++++++++++++++++++++++------ src/result.rs | 6 ++ src/scope.rs | 65 ++++++++++----- tests/chars.rs | 4 +- tests/constants.rs | 16 ++++ 9 files changed, 426 insertions(+), 155 deletions(-) create mode 100644 tests/constants.rs 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(()) +} From e5ed2f4be5eda126f621d615d270ffed1bb71937 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 13 Mar 2020 23:09:45 +0800 Subject: [PATCH 02/21] Update api.rs --- src/api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 919e6460..c12d86bf 100644 --- a/src/api.rs +++ b/src/api.rs @@ -112,7 +112,7 @@ impl<'e> Engine<'e> { let mut contents = String::new(); f.read_to_string(&mut contents) - .map_err(|err| EvalAltResult::ErrorReadingScriptFile(filename.into(), err)) + .map_err(|err| EvalAltResult::ErrorReadingScriptFile(path.clone(), err)) .map(|_| contents) } @@ -210,7 +210,7 @@ impl<'e> Engine<'e> { path: PathBuf, retain_functions: bool, ) -> Result<(), EvalAltResult> { - Self::read_file(path).and_then(|_| self.consume(&contents, retain_functions)) + Self::read_file(path).and_then(|contents| self.consume(&contents, retain_functions)) } /// Evaluate a string, but throw away the result and only return error (if any). From d5adee22099183cc118f35d689d94980388381a2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 11:51:45 +0800 Subject: [PATCH 03/21] Allow chained assignments. --- src/api.rs | 10 +++--- src/engine.rs | 14 ++++++-- src/optimize.rs | 27 +++++++++++++-- src/parser.rs | 89 ++++++++++++++++++++++++++++++++----------------- 4 files changed, 100 insertions(+), 40 deletions(-) diff --git a/src/api.rs b/src/api.rs index cf03125f..699cf2e2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -171,13 +171,15 @@ impl<'e> Engine<'e> { statements }; - let result = statements - .iter() - .try_fold(().into_dynamic(), |_, stmt| engine.eval_stmt(scope, stmt)); + let mut result = ().into_dynamic(); + + for stmt in statements { + result = engine.eval_stmt(scope, stmt)?; + } engine.clear_functions(); - result + Ok(result) } match eval_ast_internal(self, scope, ast) { diff --git a/src/engine.rs b/src/engine.rs index af3f4606..86c9a0d9 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -796,8 +796,8 @@ impl Engine<'_> { // name = rhs Expr::Variable(name, pos) => match scope.get(name) { Some((idx, _, VariableType::Normal, _)) => { - *scope.get_mut(name, idx) = rhs_val; - Ok(().into_dynamic()) + *scope.get_mut(name, idx) = rhs_val.clone(); + Ok(rhs_val) } Some((_, _, VariableType::Constant, _)) => Err( EvalAltResult::ErrorAssignmentToConstant(name.to_string(), *op_pos), @@ -947,7 +947,15 @@ impl Engine<'_> { Stmt::Noop(_) => Ok(().into_dynamic()), // Expression as statement - Stmt::Expr(expr) => self.eval_expr(scope, expr), + Stmt::Expr(expr) => { + let result = self.eval_expr(scope, expr)?; + + Ok(match expr.as_ref() { + // If it is an assignment, erase the result at the root + Expr::Assignment(_, _, _) => ().into_dynamic(), + _ => result, + }) + } // Block scope Stmt::Block(block, _) => { diff --git a/src/optimize.rs b/src/optimize.rs index 102839d9..2e11e075 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -217,9 +217,30 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } stmt => Expr::Stmt(Box::new(stmt), pos), }, - Expr::Assignment(id, expr, pos) => { - Expr::Assignment(id, Box::new(optimize_expr(*expr, state)), pos) - } + Expr::Assignment(id1, expr1, pos1) => match *expr1 { + Expr::Assignment(id2, expr2, pos2) => match (*id1, *id2) { + (Expr::Variable(var1, _), Expr::Variable(var2, _)) if var1 == var2 => { + // Assignment to the same variable - fold + state.set_dirty(); + + Expr::Assignment( + Box::new(Expr::Variable(var1, pos1)), + Box::new(optimize_expr(*expr2, state)), + pos1, + ) + } + (id1, id2) => Expr::Assignment( + Box::new(id1), + Box::new(Expr::Assignment( + Box::new(id2), + Box::new(optimize_expr(*expr2, state)), + pos2, + )), + pos1, + ), + }, + expr => Expr::Assignment(id1, Box::new(optimize_expr(expr, state)), pos1), + }, Expr::Dot(lhs, rhs, pos) => Expr::Dot( Box::new(optimize_expr(*lhs, state)), Box::new(optimize_expr(*rhs, state)), diff --git a/src/parser.rs b/src/parser.rs index 0b0e0018..d1c0a39d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1177,7 +1177,7 @@ pub fn lex(input: &str) -> TokenIterator<'_> { } } -fn get_precedence(token: &Token) -> i8 { +fn get_precedence(token: &Token) -> u8 { match *token { Token::Equals | Token::PlusAssign @@ -1192,28 +1192,49 @@ fn get_precedence(token: &Token) -> i8 { | Token::ModuloAssign | Token::PowerOfAssign => 10, - Token::Or | Token::XOr | Token::Pipe => 11, + Token::Or | Token::XOr | Token::Pipe => 50, - Token::And | Token::Ampersand => 12, + Token::And | Token::Ampersand => 60, Token::LessThan | Token::LessThanEqualsTo | Token::GreaterThan | Token::GreaterThanEqualsTo | Token::EqualsTo - | Token::NotEqualsTo => 15, + | Token::NotEqualsTo => 70, - Token::Plus | Token::Minus => 20, + Token::Plus | Token::Minus => 80, - Token::Divide | Token::Multiply | Token::PowerOf => 40, + Token::Divide | Token::Multiply | Token::PowerOf => 90, - Token::LeftShift | Token::RightShift => 50, + Token::LeftShift | Token::RightShift => 100, - Token::Modulo => 60, + Token::Modulo => 110, - Token::Period => 100, + Token::Period => 120, - _ => -1, + _ => 0, + } +} + +fn is_bind_right(token: &Token) -> bool { + match *token { + Token::Equals + | Token::PlusAssign + | Token::MinusAssign + | Token::MultiplyAssign + | Token::DivideAssign + | Token::LeftShiftAssign + | Token::RightShiftAssign + | Token::AndAssign + | Token::OrAssign + | Token::XOrAssign + | Token::ModuloAssign + | Token::PowerOfAssign => true, + + Token::Period => true, + + _ => false, } } @@ -1636,39 +1657,47 @@ fn parse_op_assignment( fn parse_binary_op<'a>( input: &mut Peekable>, - precedence: i8, + parent_precedence: u8, lhs: Expr, ) -> Result { let mut current_lhs = lhs; loop { - let mut current_precedence = -1; + let (current_precedence, bind_right) = if let Some(&(ref current_op, _)) = input.peek() { + (get_precedence(current_op), is_bind_right(current_op)) + } else { + (0, false) + }; - if let Some(&(ref current_op, _)) = input.peek() { - current_precedence = get_precedence(current_op); - } - - if current_precedence < precedence { + // Bind left to the parent lhs expression if precedence is higher + // If same precedence, then check if the operator binds right + if current_precedence < parent_precedence + || (current_precedence == parent_precedence && !bind_right) + { return Ok(current_lhs); } if let Some((op_token, pos)) = input.next() { input.peek(); - let mut rhs = parse_unary(input)?; + let rhs = parse_unary(input)?; - let mut next_precedence = -1; + let next_precedence = if let Some(&(ref next_op, _)) = input.peek() { + get_precedence(next_op) + } else { + 0 + }; - if let Some(&(ref next_op, _)) = input.peek() { - next_precedence = get_precedence(next_op); - } - - if current_precedence < next_precedence { - rhs = parse_binary_op(input, current_precedence + 1, rhs)?; - } else if current_precedence >= 100 { - // Always bind right to left for precedence over 100 - rhs = parse_binary_op(input, current_precedence, rhs)?; - } + // Bind to right if the next operator has higher precedence + // If same precedence, then check if the operator binds right + let rhs = if (current_precedence == next_precedence && bind_right) + || current_precedence < next_precedence + { + parse_binary_op(input, current_precedence, rhs)? + } else { + // Otherwise bind to left (even if next operator has the same precedence) + rhs + }; current_lhs = match op_token { Token::Plus => Expr::FunctionCall("+".into(), vec![current_lhs, rhs], None, pos), @@ -1780,7 +1809,7 @@ fn parse_binary_op<'a>( fn parse_expr<'a>(input: &mut Peekable>) -> Result { let lhs = parse_unary(input)?; - parse_binary_op(input, 0, lhs) + parse_binary_op(input, 1, lhs) } fn parse_if<'a>(input: &mut Peekable>) -> Result { From b3a22d942a16bb0454b0b2622545862f7170721b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 14:30:44 +0800 Subject: [PATCH 04/21] Allow AST optimization based on external Scope. --- examples/repl.rs | 2 +- scripts/primes.rhai | 2 +- src/api.rs | 26 ++++++- src/optimize.rs | 17 +++- src/parser.rs | 47 +++++++++++- src/scope.rs | 183 +++++++++++++++++++++++++++++++++----------- 6 files changed, 222 insertions(+), 55 deletions(-) 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 cf03125f..401e4b77 100644 --- a/src/api.rs +++ b/src/api.rs @@ -100,8 +100,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(filename: &str) -> Result { @@ -117,8 +123,20 @@ impl<'e> Engine<'e> { /// Compile a file into an AST. pub fn compile_file(&self, filename: &str) -> Result { - Self::read_file(filename) - .and_then(|contents| self.compile(&contents).map_err(|err| err.into())) + self.compile_file_with_scope(&Scope::new(), filename) + } + + /// 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, + filename: &str, + ) -> Result { + Self::read_file(filename).and_then(|contents| { + self.compile_with_scope(scope, &contents) + .map_err(|err| err.into()) + }) } /// Evaluate a file. @@ -241,7 +259,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/optimize.rs b/src/optimize.rs index 102839d9..95f36186 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,5 +1,6 @@ use crate::engine::KEYWORD_DUMP_AST; use crate::parser::{Expr, Stmt}; +use crate::scope::{Scope, ScopeEntry, VariableType}; struct State { changed: bool, @@ -330,13 +331,27 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } } -pub(crate) fn optimize(statements: Vec) -> Vec { +pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { let mut result = statements; loop { 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() .enumerate() diff --git a/src/parser.rs b/src/parser.rs index 0b0e0018..89021d39 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,8 @@ use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::{optimize::optimize, scope::VariableType}; +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, @@ -2043,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(); @@ -2073,7 +2111,7 @@ fn parse_top_level<'a>( return Ok(AST( if optimize_ast { - optimize(statements) + optimize(statements, &scope) } else { statements }, @@ -2083,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) @@ -2094,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/scope.rs b/src/scope.rs index b3eef176..e41b7b67 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,10 @@ //! 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; @@ -10,6 +14,13 @@ pub enum VariableType { 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. /// @@ -31,7 +42,7 @@ pub enum VariableType { /// /// 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>, VariableType, Dynamic)>); +pub struct Scope<'a>(Vec>); impl<'a> Scope<'a> { /// Create a new Scope. @@ -50,32 +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(), VariableType::Normal, 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 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))); + 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, - key: K, - val_type: VariableType, + name: K, + var_type: VariableType, value: Dynamic, ) { - self.0.push((key.into(), val_type, value)); + 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, VariableType, Dynamic)> { - self.0 - .pop() - .map(|(key, var_type, value)| (key.to_string(), var_type, value)) + self.0.pop().map( + |ScopeEntry { + name, + var_type, + value, + .. + }| (name.to_string(), var_type, value), + ) } /// Truncate (rewind) the Scope to a previous size. @@ -89,8 +130,18 @@ impl<'a> Scope<'a> { .iter() .enumerate() .rev() // Always search a Scope in reverse order - .find(|(_, (name, _, _))| name == key) - .map(|(i, (name, var_type, value))| (i, name.as_ref(), *var_type, 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. @@ -99,50 +150,37 @@ 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_ne!( - entry.1, + entry.var_type, VariableType::Constant, "get mut of constant variable" ); - assert_eq!(entry.0, key, "incorrect key at Scope entry"); + assert_eq!(entry.name, name, "incorrect key at Scope entry"); - &mut entry.2 + &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, var_type, value)| (key.as_ref(), *var_type, 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, VariableType, Dynamic)> for Scope<'a> @@ -150,9 +188,66 @@ where K: Into>, { fn extend>(&mut self, iter: T) { - self.0.extend( - iter.into_iter() - .map(|(key, var_type, value)| (key.into(), var_type, value)), - ); + self.0 + .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) } } From 504fd56f1f50c0f531b157bb90779029963140ae Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 14:57:59 +0800 Subject: [PATCH 05/21] More documentation on chained assignment. --- scripts/primes.rhai | 3 +-- src/lib.rs | 2 +- src/parser.rs | 4 ++-- src/scope.rs | 17 +++++++++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/primes.rhai b/scripts/primes.rhai index d3892bce..4f76291f 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -5,8 +5,7 @@ const MAX_NUMBER_TO_CHECK = 10000; // 1229 primes let prime_mask = []; prime_mask.pad(MAX_NUMBER_TO_CHECK, true); -prime_mask[0] = false; -prime_mask[1] = false; +prime_mask[0] = prime_mask[1] = false; let total_primes_found = 0; diff --git a/src/lib.rs b/src/lib.rs index 0c4bb048..93d1699f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,7 +80,7 @@ pub use error::{ParseError, ParseErrorType}; pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; pub use parser::{Position, AST, FLOAT, INT}; pub use result::EvalAltResult; -pub use scope::Scope; +pub use scope::{Scope, ScopeEntry, VariableType}; #[cfg(not(feature = "no_index"))] pub use engine::Array; diff --git a/src/parser.rs b/src/parser.rs index 2934d0d7..6b48e3f5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -153,12 +153,12 @@ 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 + /// _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, + /// constant values. The script AST can be compiled just once. During actual 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 { diff --git a/src/scope.rs b/src/scope.rs index e41b7b67..291af905 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -8,21 +8,29 @@ use crate::parser::FLOAT; use std::borrow::Cow; +/// Type of a variable in the Scope. #[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)] pub enum VariableType { + /// Normal variable. Normal, + /// Immutable constant value. Constant, } +/// An entry in the Scope. pub struct ScopeEntry<'a> { + /// Name of the variable. pub name: Cow<'a, str>, + /// Type of the variable. pub var_type: VariableType, + /// Current value of the variable. pub value: Dynamic, + /// A constant expression if the initial value matches one of the recognized types. pub expr: Option, } -/// A type containing information about current scope. -/// Useful for keeping state between `Engine` runs. +/// A type containing information about the current scope. +/// Useful for keeping state between `Engine` evaluation runs. /// /// # Example /// @@ -76,6 +84,11 @@ impl<'a> Scope<'a> { } /// Add (push) a new constant to the Scope. + /// + /// Constants are immutable and cannot be assigned to. Their values never change. + /// Constants propagation is a technique used to optimize an AST. + /// However, in order to be used for optimization, constants must be in one of the recognized types: + /// `INT` (default to `i64`, `i32` if `only_i32`), `f64`, `String`, `char` and `bool`. pub fn push_constant>, T: Any>(&mut self, name: K, value: T) { let value = value.into_dynamic(); From 26bdc8ba0875a508866aa0f31062572c9f0ead4f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 19:46:44 +0800 Subject: [PATCH 06/21] FIX - fixes panic when constant array is assigned to. Refine README section on constants. --- README.md | 93 ++++++++++++++++++++++++++++++----------- src/engine.rs | 101 ++++++++++++++++++++++++++++++--------------- src/parser.rs | 14 ++++--- tests/arrays.rs | 14 +++++-- tests/constants.rs | 8 +++- 5 files changed, 161 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 6cbb3280..db33e369 100644 --- a/README.md +++ b/README.md @@ -65,15 +65,15 @@ Examples A number of examples can be found in the `examples` folder: -| Example | Description | -| -------------------------- | ------------------------------------------------------------------------- | -| `arrays_and_structs` | demonstrates registering a new type to Rhai and the usage of arrays on it | -| `custom_types_and_methods` | shows how to register a type and methods for it | -| `hello` | simple example that evaluates an expression and prints the result | -| `reuse_scope` | evaluates two pieces of code in separate runs, but using a common scope | -| `rhai_runner` | runs each filename passed to it as a Rhai script | -| `simple_fn` | shows how to register a Rust function to a Rhai engine | -| `repl` | a simple REPL, interactively evaluate statements from stdin | +| Example | Description | +| -------------------------- | --------------------------------------------------------------------------- | +| `arrays_and_structs` | demonstrates registering a new type to Rhai and the usage of arrays on it | +| `custom_types_and_methods` | shows how to register a type and methods for it | +| `hello` | simple example that evaluates an expression and prints the result | +| `reuse_scope` | evaluates two pieces of code in separate runs, but using a common [`Scope`] | +| `rhai_runner` | runs each filename passed to it as a Rhai script | +| `simple_fn` | shows how to register a Rust function to a Rhai [`Engine`] | +| `repl` | a simple REPL, interactively evaluate statements from stdin | Examples can be run with the following command: @@ -294,7 +294,7 @@ fn main() -> Result<(), EvalAltResult> } ``` -To return a `Dynamic` value, simply `Box` it and return it. +To return a [`Dynamic`] value, simply `Box` it and return it. ```rust fn decide(yes_no: bool) -> Dynamic { @@ -421,7 +421,7 @@ fn main() -> Result<(), EvalAltResult> } ``` -First, for each type we use with the engine, we need to be able to Clone. This allows the engine to pass by value and still keep its own state. +All custom types must implement `Clone`. This allows the [`Engine`] to pass by value. ```rust #[derive(Clone)] @@ -430,7 +430,7 @@ struct TestStruct { } ``` -Next, we create a few methods that we'll later use in our scripts. Notice that we register our custom type with the engine. +Next, we create a few methods that we'll later use in our scripts. Notice that we register our custom type with the [`Engine`]. ```rust impl TestStruct { @@ -448,9 +448,9 @@ let mut engine = Engine::new(); engine.register_type::(); ``` -To use methods and functions with the engine, we need to register them. There are some convenience functions to help with this. Below I register update and new with the engine. +To use methods and functions with the [`Engine`], we need to register them. There are some convenience functions to help with this. Below I register update and new with the [`Engine`]. -*Note: the engine follows the convention that methods use a &mut first parameter so that invoking methods can update the value in memory.* +*Note: [`Engine`] follows the convention that methods use a `&mut` first parameter so that invoking methods can update the value in memory.* ```rust engine.register_fn("update", TestStruct::update); @@ -530,7 +530,7 @@ println!("result: {}", result); Initializing and maintaining state --------------------------------- -By default, Rhai treats each engine invocation as a fresh one, persisting only the functions that have been defined but no top-level state. This gives each one a fairly clean starting place. Sometimes, though, you want to continue using the same top-level state from one invocation to the next. +By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions that have been defined but no top-level state. This gives each one a fairly clean starting place. Sometimes, though, you want to continue using the same top-level state from one invocation to the next. In this example, we first create a state with a few initialized variables, then thread the same state through multiple invocations: @@ -604,10 +604,17 @@ Constants can be defined and are immutable. Constants follow the same naming ru ```rust const x = 42; -print(x * 2); // prints 84 -x = 123; // <- syntax error - cannot assign to constant +print(x * 2); // prints 84 +x = 123; // <- syntax error - cannot assign to constant ``` +Constants must be assigned a _value_ not an expression. + +```rust +const x = 40 + 2; // <- syntax error - cannot assign expression to constant +``` + + Numbers ------- @@ -997,7 +1004,7 @@ fn add(x, y) { print(add(2, 3)); ``` -Functions defined in script always take `Dynamic` parameters (i.e. the parameter can be of any type). +Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). It is important to remember that all parameters are passed by _value_, so all functions are _pure_ (i.e. they never modify their parameters). Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if you are not careful. @@ -1029,7 +1036,7 @@ fn do_addition(x) { } ``` -Functions can be _overloaded_ based on the number of parameters (but not parameter types, since all parameters are `Dynamic`). +Functions can be _overloaded_ based on the number of parameters (but not parameter types, since all parameters are [`Dynamic`]). New definitions of the same name and number of parameters overwrite previous definitions. ```rust @@ -1122,24 +1129,56 @@ The above script optimizes to: Constants propagation is used to remove dead code: ```rust -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' +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 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 ``` These are quite effective for template-based machine-generated scripts where certain constant values are spliced into the script text in order to turn on/off certain sections. +For fixed script texts, the constant values can be provided in a user-defined `Scope` object to the `Engine` for use in compilation and evaluation. Beware, however, that most operators are actually function calls, and those functions can be overridden, so they are not optimized away: ```rust -if 1 == 1 { ... } // '1==1' is NOT optimized away because you can define - // your own '==' function to override the built-in default! +const DECISION = 1; + +if DECISION == 1 { // NOT optimized away because you can define + : // your own '==' function to override the built-in default! + : +} else if DECISION == 2 { // same here, NOT optimized away + : +} else if DECISION == 3 { // same here, NOT optimized away + : +} else { + : +} ``` -### Here be dragons! +So, instead, do this: + +```rust +const DECISION_1 = true; +const DECISION_2 = false; +const DECISION_3 = false; + +if DECISION_1 { + : // this branch is kept +} else if DECISION_2 { + : // this branch is eliminated +} else if DECISION_3 { + : // this branch is eliminated +} else { + : // this branch is eliminated +} +``` + +In general, boolean constants are most effective if you want the optimizer to automatically prune large `if`-`else` branches because they do not depend on operators. + +Here be dragons! +---------------- Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example: @@ -1191,3 +1230,7 @@ engine.set_optimization(false); // turn off the optimizer [`no_function`]: #optional-features [`only_i32`]: #optional-features [`only_i64`]: #optional-features + +[`Engine`]: #hello-world +[`Scope`]: #initializing-and-maintaining-state +[`Dynamic`]: #values-and-types diff --git a/src/engine.rs b/src/engine.rs index 86c9a0d9..36d7fc71 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -372,16 +372,26 @@ impl Engine<'_> { 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. - if let Some((id, src_idx)) = src { - Self::update_indexed_var_in_scope( - src_type, - scope, - id, - src_idx, - idx, - target, - idx_lhs.position(), - )?; + if let Some((id, var_type, src_idx)) = src { + match var_type { + VariableType::Constant => { + return Err(EvalAltResult::ErrorAssignmentToConstant( + id.to_string(), + idx_lhs.position(), + )); + } + VariableType::Normal => { + Self::update_indexed_var_in_scope( + src_type, + scope, + id, + src_idx, + idx, + target, + dot_rhs.position(), + )?; + } + } } val @@ -477,7 +487,15 @@ impl Engine<'_> { lhs: &'a Expr, idx_expr: &Expr, idx_pos: Position, - ) -> Result<(IndexSourceType, Option<(&'a str, usize)>, usize, Dynamic), EvalAltResult> { + ) -> Result< + ( + IndexSourceType, + Option<(&'a str, VariableType, usize)>, + usize, + Dynamic, + ), + EvalAltResult, + > { let idx = self.eval_index_value(scope, idx_expr)?; match lhs { @@ -488,8 +506,13 @@ impl Engine<'_> { |val| self.get_indexed_value(&val, idx, idx_expr.position(), idx_pos), lhs.position(), ) - .map(|(src_idx, _, (val, src_type))| { - (src_type, Some((id.as_str(), src_idx)), idx as usize, val) + .map(|(src_idx, var_type, (val, src_type))| { + ( + src_type, + Some((id.as_str(), var_type, src_idx)), + idx as usize, + val, + ) }), // (expr)[idx_expr] @@ -739,16 +762,20 @@ impl Engine<'_> { self.set_dot_val_helper(scope, target.as_mut(), dot_rhs, new_val, val_pos); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. - if let Some((id, src_idx)) = src { - Self::update_indexed_var_in_scope( - src_type, - scope, - id, - src_idx, - idx, - target, - lhs.position(), - )?; + if let Some((id, var_type, src_idx)) = src { + match var_type { + VariableType::Constant => { + return Err(EvalAltResult::ErrorAssignmentToConstant( + id.to_string(), + lhs.position(), + )); + } + VariableType::Normal => { + Self::update_indexed_var_in_scope( + src_type, scope, id, src_idx, idx, target, val_pos, + )?; + } + } } val @@ -811,16 +838,24 @@ impl Engine<'_> { let (src_type, src, idx, _) = self.eval_index_expr(scope, idx_lhs, idx_expr, *idx_pos)?; - if let Some((id, src_idx)) = src { - Ok(Self::update_indexed_var_in_scope( - src_type, - scope, - &id, - src_idx, - idx, - rhs_val, - rhs.position(), - )?) + if let Some((id, var_type, src_idx)) = src { + match var_type { + VariableType::Constant => { + return Err(EvalAltResult::ErrorAssignmentToConstant( + id.to_string(), + idx_lhs.position(), + )); + } + VariableType::Normal => Ok(Self::update_indexed_var_in_scope( + src_type, + scope, + &id, + src_idx, + idx, + rhs_val, + rhs.position(), + )?), + } } else { Err(EvalAltResult::ErrorAssignmentToUnknownLHS( idx_lhs.position(), diff --git a/src/parser.rs b/src/parser.rs index 6b48e3f5..e660d0c6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -343,6 +343,8 @@ impl Expr { | Expr::False(_) | Expr::Unit(_) => true, + Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant), + #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, _) => true, @@ -1635,12 +1637,12 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result 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::Index(idx_lhs, _, pos) => Some(ParseError::new( + match idx_lhs.as_ref() { + Expr::Index(_, _, _) => ParseErrorType::AssignmentToCopy, + _ => ParseErrorType::AssignmentToInvalidLHS, + }, + *pos, )), Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { diff --git a/tests/arrays.rs b/tests/arrays.rs index 4bee090e..d8c6a38d 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -7,6 +7,10 @@ fn test_arrays() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::("let x = [1, 2, 3]; x[1]")?, 2); assert_eq!(engine.eval::("let y = [1, 2, 3]; y[1] = 5; y[1]")?, 5); + assert_eq!( + engine.eval::(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?, + '3' + ); Ok(()) } @@ -48,10 +52,12 @@ fn test_array_with_structs() -> Result<(), EvalAltResult> { assert_eq!( engine.eval::( - "let a = [new_ts()]; \ - a[0].x = 100; \ - a[0].update(); \ - a[0].x", + r" + let a = [new_ts()]; + a[0].x = 100; + a[0].update(); + a[0].x + " )?, 1100 ); diff --git a/tests/constants.rs b/tests/constants.rs index e81f7ce4..ff668797 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -6,7 +6,13 @@ fn test_constant() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::("const x = 123; x")?, 123); - match engine.eval::("const x = 123; x = 42; x") { + match engine.eval::("const x = 123; x = 42;") { + Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (), + Err(err) => return Err(err), + Ok(_) => panic!("expecting compilation error"), + } + + match engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;") { Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (), Err(err) => return Err(err), Ok(_) => panic!("expecting compilation error"), From 973153e8325907fb9ec1057f8d23342ad57afbc8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 20:06:10 +0800 Subject: [PATCH 07/21] Add no_optimize feature to disable optimizations. --- Cargo.toml | 1 + README.md | 3 +++ src/parser.rs | 53 ++++++++++++++++++++++++++++++++------------------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a5177aa6..4ed917d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ no_stdlib = [] # no standard library of utility functions no_index = [] # no arrays and indexing no_float = [] # no floating-point no_function = [] # no script-defined functions +no_optimize = [] # no script optimizer only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types diff --git a/README.md b/README.md index db33e369..b5870ee1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Optional features | `no_function` | Disable script-defined functions if you don't need them. | | `no_index` | Disable arrays and indexing features if you don't need them. | | `no_float` | Disable floating-point numbers and math if you don't need them. | +| `no_optimize` | Disable the script optimizer. | | `only_i32` | Set the system integer type to `i32` and disable all other integer types. | | `only_i64` | Set the system integer type to `i64` and disable all other integer types. | @@ -1099,6 +1100,7 @@ Optimizations ============= Rhai includes an _optimizer_ that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed. +Script optimization can be turned off via the [`no_optimize`] feature. For example, in the following: @@ -1228,6 +1230,7 @@ engine.set_optimization(false); // turn off the optimizer [`no_index`]: #optional-features [`no_float`]: #optional-features [`no_function`]: #optional-features +[`no_optimize`]: #optional-features [`only_i32`]: #optional-features [`only_i64`]: #optional-features diff --git a/src/parser.rs b/src/parser.rs index e660d0c6..daebaf13 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,9 +2,11 @@ use crate::any::Dynamic; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::optimize::optimize; use crate::scope::{Scope, VariableType}; +#[cfg(not(feature = "no_optimize"))] +use crate::optimize::optimize; + use std::{ borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc, usize, @@ -23,6 +25,7 @@ pub type INT = i64; pub type INT = i32; /// The system floating-point type +#[cfg(not(feature = "no_float"))] pub type FLOAT = f64; type LERR = LexError; @@ -161,6 +164,7 @@ impl AST { /// constant values. The script AST can be compiled just once. During actual 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. + #[cfg(not(feature = "no_optimize"))] pub fn optimize(self, scope: &Scope) -> Self { AST( crate::optimize::optimize(self.0, scope), @@ -2140,25 +2144,34 @@ fn parse_top_level<'a>( } } - return Ok(AST( - if optimize_ast { - optimize(statements, &scope) - } else { - statements - }, - #[cfg(not(feature = "no_function"))] - functions - .into_iter() - .map(|mut fn_def| { - if optimize_ast { - let pos = fn_def.body.position(); - let mut body = optimize(vec![fn_def.body], &scope); - fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); - } - Arc::new(fn_def) - }) - .collect(), - )); + Ok( + #[cfg(not(feature = "no_optimize"))] + AST( + if optimize_ast { + optimize(statements, &scope) + } else { + statements + }, + #[cfg(not(feature = "no_function"))] + functions + .into_iter() + .map(|mut fn_def| { + if optimize_ast { + let pos = fn_def.body.position(); + let mut body = optimize(vec![fn_def.body], &scope); + fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); + } + Arc::new(fn_def) + }) + .collect(), + ), + #[cfg(feature = "no_optimize")] + AST( + statements, + #[cfg(not(feature = "no_function"))] + functions.into_iter().map(Arc::new).collect(), + ), + ) } pub fn parse<'a>( From dd36f3387a57810950fb33f6bea16122dfd95129 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 20:06:40 +0800 Subject: [PATCH 08/21] Fixup code to make sure all feature builds succeed. --- src/builtin.rs | 4 +++- src/call.rs | 2 ++ src/lib.rs | 5 ++++- src/optimize.rs | 2 ++ src/scope.rs | 23 ++++++++++++++--------- tests/constants.rs | 1 + tests/power_of.rs | 5 ++++- 7 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/builtin.rs b/src/builtin.rs index da139f60..b69429f4 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -8,7 +8,9 @@ use crate::engine::Engine; use crate::fn_register::{RegisterFn, RegisterResultFn}; use crate::parser::{Position, INT}; use crate::result::EvalAltResult; -use crate::FLOAT; + +#[cfg(not(feature = "no_float"))] +use crate::parser::FLOAT; use num_traits::{ identities::Zero, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedShl, diff --git a/src/call.rs b/src/call.rs index 9a2252c0..2a2822eb 100644 --- a/src/call.rs +++ b/src/call.rs @@ -1,6 +1,8 @@ //! Helper module which defines `FnArgs` to make function calling easier. use crate::any::{Any, Dynamic}; + +#[cfg(not(feature = "no_index"))] use crate::engine::Array; /// Trait that represent arguments to a function call. diff --git a/src/lib.rs b/src/lib.rs index 93d1699f..47be664d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,9 +78,12 @@ pub use call::FuncArgs; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; -pub use parser::{Position, AST, FLOAT, INT}; +pub use parser::{Position, AST, INT}; pub use result::EvalAltResult; pub use scope::{Scope, ScopeEntry, VariableType}; #[cfg(not(feature = "no_index"))] pub use engine::Array; + +#[cfg(not(feature = "no_float"))] +pub use parser::FLOAT; diff --git a/src/optimize.rs b/src/optimize.rs index 2e2e7778..f2352afa 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,3 +1,5 @@ +#![cfg(not(feature = "no_optimize"))] + use crate::engine::KEYWORD_DUMP_AST; use crate::parser::{Expr, Stmt}; use crate::scope::{Scope, ScopeEntry, VariableType}; diff --git a/src/scope.rs b/src/scope.rs index 291af905..081237fd 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -221,15 +221,6 @@ fn map_dynamic_to_expr(value: Dynamic) -> (Option, Dynamic) { )), 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(); ( @@ -261,6 +252,20 @@ fn map_dynamic_to_expr(value: Dynamic) -> (Option, Dynamic) { value2, ) } else { + #[cfg(not(feature = "no_float"))] + { + if value.is::() { + let value2 = value.clone(); + return ( + Some(Expr::FloatConstant( + *value.downcast::().expect("value should be FLOAT"), + Position::none(), + )), + value2, + ); + } + } + (None, value) } } diff --git a/tests/constants.rs b/tests/constants.rs index ff668797..f931e76b 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -12,6 +12,7 @@ fn test_constant() -> Result<(), EvalAltResult> { Ok(_) => panic!("expecting compilation error"), } + #[cfg(not(feature = "no_index"))] match engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;") { Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (), Err(err) => return Err(err), diff --git a/tests/power_of.rs b/tests/power_of.rs index 8e1ed522..f02a2e52 100644 --- a/tests/power_of.rs +++ b/tests/power_of.rs @@ -1,4 +1,7 @@ -use rhai::{Engine, EvalAltResult, FLOAT, INT}; +use rhai::{Engine, EvalAltResult, INT}; + +#[cfg(not(feature = "no_float"))] +use rhai::FLOAT; #[test] fn test_power_of() -> Result<(), EvalAltResult> { From b87dc1b281572ea5998c87c032e510fcfdb4c3a2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 20:08:18 +0800 Subject: [PATCH 09/21] Only map to expressions for constants. --- src/scope.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scope.rs b/src/scope.rs index 081237fd..b20eb241 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -73,13 +73,13 @@ impl<'a> Scope<'a> { let value = value.into_dynamic(); // Map into constant expressions - let (expr, value) = map_dynamic_to_expr(value); + //let (expr, value) = map_dynamic_to_expr(value); self.0.push(ScopeEntry { name: name.into(), var_type: VariableType::Normal, value, - expr, + expr: None, }); } @@ -110,13 +110,13 @@ impl<'a> Scope<'a> { var_type: VariableType, value: Dynamic, ) { - let (expr, value) = map_dynamic_to_expr(value); + //let (expr, value) = map_dynamic_to_expr(value); self.0.push(ScopeEntry { name: name.into(), var_type, value, - expr, + expr: None, }); } From b9e404063528b93ea60167c6a46a5abe6d72fa0b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 23:39:45 +0800 Subject: [PATCH 10/21] Deprecate Error::description. --- src/error.rs | 37 ++++++++++--------------------------- src/result.rs | 14 ++++++-------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/error.rs b/src/error.rs index e769d993..89d6f370 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,18 +20,7 @@ pub enum LexError { InputError(String), } -impl Error for LexError { - fn description(&self) -> &str { - match *self { - Self::UnexpectedChar(_) => "Unexpected character", - Self::UnterminatedString => "Open string is not terminated", - Self::MalformedEscapeSequence(_) => "Unexpected values in escape sequence", - Self::MalformedNumber(_) => "Unexpected characters in number", - Self::MalformedChar(_) => "Char constant not a single character", - Self::InputError(_) => "Input error", - } - } -} +impl Error for LexError {} impl fmt::Display for LexError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -41,7 +30,7 @@ impl fmt::Display for LexError { Self::MalformedNumber(s) => write!(f, "Invalid number: '{}'", s), Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s), Self::InputError(s) => write!(f, "{}", s), - _ => write!(f, "{}", self.description()), + Self::UnterminatedString => write!(f, "Open string is not terminated"), } } } @@ -104,10 +93,8 @@ impl ParseError { pub fn position(&self) -> Position { self.1 } -} -impl Error for ParseError { - fn description(&self) -> &str { + pub(crate) fn desc(&self) -> &str { match self.0 { ParseErrorType::BadInput(ref p) => p, ParseErrorType::InputPastEndOfFile => "Script is incomplete", @@ -128,39 +115,35 @@ impl Error for ParseError { ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable." } } - - fn cause(&self) -> Option<&dyn Error> { - None - } } +impl Error for ParseError {} + impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { ParseErrorType::BadInput(ref s) | ParseErrorType::MalformedIndexExpr(ref s) | ParseErrorType::MalformedCallExpr(ref s) => { - write!(f, "{}", if s.is_empty() { self.description() } else { s })? + write!(f, "{}", if s.is_empty() { self.desc() } else { s })? } ParseErrorType::ForbiddenConstantExpr(ref s) => { write!(f, "Expecting a constant to assign to '{}'", s)? } - ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.description(), s)?, + ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.desc(), s)?, ParseErrorType::FnMissingParams(ref s) => { write!(f, "Expecting parameters for function '{}'", s)? } ParseErrorType::MissingRightParen(ref s) | ParseErrorType::MissingRightBrace(ref s) - | ParseErrorType::MissingRightBracket(ref s) => { - write!(f, "{} for {}", self.description(), s)? - } + | ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { - write!(f, "{}", self.description())? + write!(f, "{}", self.desc())? } ParseErrorType::AssignmentToConstant(ref s) => { write!(f, "Cannot assign to constant '{}'", s)? } - _ => write!(f, "{}", self.description())?, + _ => write!(f, "{}", self.desc())?, } if !self.1.is_eof() { diff --git a/src/result.rs b/src/result.rs index acafecc4..faa8db7c 100644 --- a/src/result.rs +++ b/src/result.rs @@ -61,10 +61,10 @@ pub enum EvalAltResult { Return(Dynamic, Position), } -impl Error for EvalAltResult { - fn description(&self) -> &str { +impl EvalAltResult { + pub(crate) fn desc(&self) -> &str { match self { - Self::ErrorParsing(p) => p.description(), + Self::ErrorParsing(p) => p.desc(), Self::ErrorFunctionNotFound(_, _) => "Function not found", Self::ErrorFunctionArgsMismatch(_, _, _, _) => { "Function call with wrong number of arguments" @@ -101,15 +101,13 @@ impl Error for EvalAltResult { Self::Return(_, _) => "[Not Error] Function returns value", } } - - fn cause(&self) -> Option<&dyn Error> { - None - } } +impl Error for EvalAltResult {} + impl fmt::Display for EvalAltResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let desc = self.description(); + let desc = self.desc(); match self { Self::ErrorFunctionNotFound(s, pos) => write!(f, "{}: '{}' ({})", desc, s, pos), From 6e076c409dea77be769b4d74baf177a9e520b928 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 23:40:30 +0800 Subject: [PATCH 11/21] Use matches! in examples. --- examples/repl.rs | 9 ++-- tests/constants.rs | 18 +++---- tests/decrement.rs | 9 ++-- tests/math.rs | 110 ++++++++++++++++++++++++----------------- tests/mismatched_op.rs | 18 +++---- tests/throw.rs | 18 +++---- 6 files changed, 98 insertions(+), 84 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 7e32ec16..68ae0c96 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -62,10 +62,11 @@ fn main() { match input.as_str().trim() { "exit" | "quit" => break, // quit "ast" => { - // print the last AST - match &ast { - Some(ast) => println!("{:#?}", ast), - None => println!("()"), + if matches!(&ast, Some(_)) { + // print the last AST + println!("{:#?}", ast.as_ref().unwrap()); + } else { + println!("()"); } continue; } diff --git a/tests/constants.rs b/tests/constants.rs index f931e76b..9a93c119 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -6,18 +6,16 @@ fn test_constant() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::("const x = 123; x")?, 123); - match engine.eval::("const x = 123; x = 42;") { - Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (), - Err(err) => return Err(err), - Ok(_) => panic!("expecting compilation error"), - } + assert!( + matches!(engine.eval::("const x = 123; x = 42;").expect_err("expects error"), + EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x") + ); #[cfg(not(feature = "no_index"))] - match engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;") { - Err(EvalAltResult::ErrorAssignmentToConstant(var, _)) if var == "x" => (), - Err(err) => return Err(err), - Ok(_) => panic!("expecting compilation error"), - } + assert!( + matches!(engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), + EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x") + ); Ok(()) } diff --git a/tests/decrement.rs b/tests/decrement.rs index 3547adc4..d653a7aa 100644 --- a/tests/decrement.rs +++ b/tests/decrement.rs @@ -6,12 +6,9 @@ fn test_decrement() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::("let x = 10; x -= 7; x")?, 3); - let r = engine.eval::("let s = \"test\"; s -= \"ing\"; s"); - - match r { - Err(EvalAltResult::ErrorFunctionNotFound(err, _)) if err == "- (string, string)" => (), - _ => panic!(), - } + assert!(matches!(engine + .eval::(r#"let s = "test"; s -= "ing"; s"#) + .expect_err("expects error"), EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (string, string)")); Ok(()) } diff --git a/tests/math.rs b/tests/math.rs index e9565b39..49b06e35 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -24,54 +24,76 @@ fn test_math() -> Result<(), EvalAltResult> { { #[cfg(not(feature = "only_i32"))] { - match engine.eval::("(-9223372036854775808).abs()") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("9223372036854775807 + 1") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("-9223372036854775808 - 1") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return underflow error: {:?}", r), - } - match engine.eval::("9223372036854775807 * 9223372036854775807") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("9223372036854775807 / 0") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return division by zero error: {:?}", r), - } - match engine.eval::("9223372036854775807 % 0") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return division by zero error: {:?}", r), - } + assert!(matches!( + engine + .eval::("(-9223372036854775808).abs()") + .expect_err("expects negation overflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("9223372036854775807 + 1") + .expect_err("expects overflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("-9223372036854775808 - 1") + .expect_err("expects underflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("9223372036854775807 * 9223372036854775807") + .expect_err("expects overflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("9223372036854775807 / 0") + .expect_err("expects division by zero"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("9223372036854775807 % 0") + .expect_err("expects division by zero"), + EvalAltResult::ErrorArithmetic(_, _) + )); } #[cfg(feature = "only_i32")] { - match engine.eval::("2147483647 + 1") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("-2147483648 - 1") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return underflow error: {:?}", r), - } - match engine.eval::("2147483647 * 2147483647") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return overflow error: {:?}", r), - } - match engine.eval::("2147483647 / 0") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return division by zero error: {:?}", r), - } - match engine.eval::("2147483647 % 0") { - Err(EvalAltResult::ErrorArithmetic(_, _)) => (), - r => panic!("should return division by zero error: {:?}", r), - } + assert!(matches!( + engine + .eval::("2147483647 + 1") + .expect_err("expects overflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("-2147483648 - 1") + .expect_err("expects underflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("2147483647 * 2147483647") + .expect_err("expects overflow"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("2147483647 / 0") + .expect_err("expects division by zero"), + EvalAltResult::ErrorArithmetic(_, _) + )); + assert!(matches!( + engine + .eval::("2147483647 % 0") + .expect_err("expects division by zero"), + EvalAltResult::ErrorArithmetic(_, _) + )); } } diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 1abb3cc1..861ab4c4 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -5,12 +5,10 @@ use rhai::{Engine, EvalAltResult, RegisterFn, INT}; fn test_mismatched_op() { let mut engine = Engine::new(); - let r = engine.eval::("60 + \"hello\""); - - match r { - Err(EvalAltResult::ErrorMismatchOutputType(err, _)) if err == "string" => (), - _ => panic!(), - } + assert!( + matches!(engine.eval::(r#"60 + "hello""#).expect_err("expects error"), + EvalAltResult::ErrorMismatchOutputType(err, _) if err == "string") + ); } #[test] @@ -30,14 +28,16 @@ fn test_mismatched_op_custom_type() { engine.register_type_with_name::("TestStruct"); engine.register_fn("new_ts", TestStruct::new); - let r = engine.eval::("60 + new_ts()"); + let r = engine + .eval::("60 + new_ts()") + .expect_err("expects error"); match r { #[cfg(feature = "only_i32")] - Err(EvalAltResult::ErrorFunctionNotFound(err, _)) if err == "+ (i32, TestStruct)" => (), + EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)" => (), #[cfg(not(feature = "only_i32"))] - Err(EvalAltResult::ErrorFunctionNotFound(err, _)) if err == "+ (i64, TestStruct)" => (), + EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)" => (), _ => panic!(), } diff --git a/tests/throw.rs b/tests/throw.rs index 5edecedc..518f56d8 100644 --- a/tests/throw.rs +++ b/tests/throw.rs @@ -1,18 +1,14 @@ -use rhai::{Engine, EvalAltResult, INT}; +use rhai::{Engine, EvalAltResult}; #[test] fn test_throw() { let mut engine = Engine::new(); - match engine.eval::(r#"if true { throw "hello" }"#) { - Ok(_) => panic!("not an error"), - Err(EvalAltResult::ErrorRuntime(s, _)) if s == "hello" => (), - Err(err) => panic!("wrong error: {}", err), - } + assert!(matches!( + engine.eval::<()>(r#"if true { throw "hello" }"#).expect_err("expects error"), + EvalAltResult::ErrorRuntime(s, _) if s == "hello")); - match engine.eval::(r#"throw;"#) { - Ok(_) => panic!("not an error"), - Err(EvalAltResult::ErrorRuntime(s, _)) if s == "" => (), - Err(err) => panic!("wrong error: {}", err), - } + assert!(matches!( + engine.eval::<()>(r#"throw;"#).expect_err("expects error"), + EvalAltResult::ErrorRuntime(s, _) if s == "")); } From 01cf7779618ab3131806ccd411d8f98ee56fc368 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 14 Mar 2020 23:41:15 +0800 Subject: [PATCH 12/21] Use matches! macro. --- src/engine.rs | 15 +++++---- src/optimize.rs | 28 ++++++--------- src/parser.rs | 90 ++++++++++++++++++++----------------------------- 3 files changed, 54 insertions(+), 79 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 36d7fc71..f1d9eecd 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -153,8 +153,8 @@ impl Engine<'_> { ); // Evaluate + // Convert return statement to return value return match self.eval_stmt(&mut scope, &fn_def.body) { - // Convert return statement to return value Err(EvalAltResult::Return(x, _)) => Ok(x), other => other, }; @@ -985,10 +985,11 @@ impl Engine<'_> { Stmt::Expr(expr) => { let result = self.eval_expr(scope, expr)?; - Ok(match expr.as_ref() { + Ok(if !matches!(expr.as_ref(), Expr::Assignment(_, _, _)) { + result + } else { // If it is an assignment, erase the result at the root - Expr::Assignment(_, _, _) => ().into_dynamic(), - _ => result, + ().into_dynamic() }) } @@ -1032,9 +1033,9 @@ impl Engine<'_> { Ok(guard_val) => { if *guard_val { match self.eval_stmt(scope, body) { + Ok(_) => (), Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), Err(x) => return Err(x), - _ => (), } } else { return Ok(().into_dynamic()); @@ -1047,9 +1048,9 @@ impl Engine<'_> { // Loop statement Stmt::Loop(body) => loop { match self.eval_stmt(scope, body) { + Ok(_) => (), Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), Err(x) => return Err(x), - _ => (), } }, @@ -1066,9 +1067,9 @@ impl Engine<'_> { *scope.get_mut(name, idx) = a; match self.eval_stmt(scope, body) { + Ok(_) => (), Err(EvalAltResult::LoopBreak) => break, Err(x) => return Err(x), - _ => (), } } scope.pop(); diff --git a/src/optimize.rs b/src/optimize.rs index f2352afa..6013ea01 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -50,16 +50,15 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { let pos = expr.position(); let expr = optimize_expr(*expr, state); - match expr { - Expr::False(_) | Expr::True(_) => Stmt::Noop(stmt1.position()), - expr => { - let stmt = Stmt::Expr(Box::new(expr)); + if matches!(expr, Expr::False(_) | Expr::True(_)) { + Stmt::Noop(stmt1.position()) + } else { + let stmt = Stmt::Expr(Box::new(expr)); - if preserve_result { - Stmt::Block(vec![stmt, *stmt1], pos) - } else { - stmt - } + if preserve_result { + Stmt::Block(vec![stmt, *stmt1], pos) + } else { + stmt } } } @@ -136,10 +135,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { // Remove all raw expression statements that are pure except for the very last statement let last_stmt = if preserve_result { result.pop() } else { None }; - result.retain(|stmt| match stmt { - Stmt::Expr(expr) if expr.is_pure() => false, - _ => true, - }); + result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); if let Some(stmt) = last_stmt { result.push(stmt); @@ -154,7 +150,6 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { match expr { Stmt::Let(_, None, _) => removed = true, Stmt::Let(_, Some(val_expr), _) if val_expr.is_pure() => removed = true, - _ => { result.push(expr); break; @@ -402,10 +397,7 @@ pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { let last_stmt = result.pop(); // Remove all pure statements at top level - result.retain(|stmt| match stmt { - Stmt::Expr(expr) if expr.is_pure() => false, - _ => true, - }); + result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); if let Some(stmt) = last_stmt { result.push(stmt); // Add back the last statement diff --git a/src/parser.rs b/src/parser.rs index daebaf13..e1b8d9da 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -229,24 +229,15 @@ pub enum Stmt { impl Stmt { pub fn is_noop(&self) -> bool { - match self { - Stmt::Noop(_) => true, - _ => false, - } + matches!(self, Stmt::Noop(_)) } pub fn is_op(&self) -> bool { - match self { - Stmt::Noop(_) => false, - _ => true, - } + !matches!(self, Stmt::Noop(_)) } pub fn is_var(&self) -> bool { - match self { - Stmt::Let(_, _, _) => true, - _ => false, - } + matches!(self, Stmt::Let(_, _, _)) } pub fn position(&self) -> Position { @@ -334,7 +325,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_variable(), + expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)), } } @@ -355,20 +346,6 @@ impl Expr { _ => false, } } - - pub fn is_variable(&self) -> bool { - match self { - Expr::Variable(_, _) => true, - _ => false, - } - } - - pub fn is_property(&self) -> bool { - match self { - Expr::Property(_, _) => true, - _ => false, - } - } } #[derive(Debug, PartialEq, Clone)] @@ -1629,13 +1606,13 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result { + Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) => { assert!(is_top, "property expected but gets variable"); None } #[cfg(not(feature = "no_index"))] - Expr::Index(idx_lhs, _, _) if idx_lhs.is_property() => { + Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) => { assert!(!is_top, "variable expected but gets property"); None } @@ -1655,7 +1632,13 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result + if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) && is_top => + { + valid_assignment_chain(dot_rhs, false) + } + #[cfg(not(feature = "no_index"))] + Expr::Index(idx_lhs, _, _) + if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) && !is_top => { valid_assignment_chain(dot_rhs, false) } @@ -1865,9 +1848,10 @@ fn parse_if<'a>(input: &mut Peekable>) -> Result { input.next(); - let else_body = match input.peek() { - Some(&(Token::If, _)) => parse_if(input)?, - _ => parse_block(input)?, + let else_body = if matches!(input.peek(), Some(&(Token::If, _))) { + parse_if(input)? + } else { + parse_block(input)? }; Ok(Stmt::IfElse( @@ -1934,25 +1918,24 @@ fn parse_var<'a>( None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())), }; - match input.peek() { - Some(&(Token::Equals, _)) => { - input.next(); - let init_value = parse_expr(input)?; + if matches!(input.peek(), Some(&(Token::Equals, _))) { + input.next(); + let init_value = parse_expr(input)?; - match var_type { - VariableType::Normal => 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(), - )), + 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)), + } else { + Ok(Stmt::Let(name, None, pos)) } } @@ -2072,11 +2055,10 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result { - input.next(); - } - _ => loop { + if matches!(input.peek(), Some(&(Token::RightParen, _))) { + input.next(); + } else { + loop { match input.next() { Some((Token::RightParen, _)) => break, Some((Token::Comma, _)) => (), @@ -2100,7 +2082,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result Date: Sat, 14 Mar 2020 23:41:21 +0800 Subject: [PATCH 13/21] Minor code cleanup. --- src/builtin.rs | 11 +++++++---- src/fn_register.rs | 11 ++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/builtin.rs b/src/builtin.rs index b69429f4..b85cf306 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -580,7 +580,7 @@ impl Engine<'_> { } fn range(from: T, to: T) -> Range { - (from..to) + from..to } reg_iterator::(self); @@ -773,9 +773,12 @@ impl Engine<'_> { self.register_dynamic_fn("pop", |list: &mut Array| { list.pop().unwrap_or_else(|| ().into_dynamic()) }); - self.register_dynamic_fn("shift", |list: &mut Array| match list.len() { - 0 => ().into_dynamic(), - _ => list.remove(0), + self.register_dynamic_fn("shift", |list: &mut Array| { + if !list.is_empty() { + ().into_dynamic() + } else { + list.remove(0) + } }); self.register_fn("len", |list: &mut Array| list.len() as INT); self.register_fn("clear", |list: &mut Array| list.clear()); diff --git a/src/fn_register.rs b/src/fn_register.rs index 84dbf150..12f15047 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -196,13 +196,10 @@ macro_rules! def_register { // Call the user-supplied function using ($clone) to // potentially clone the value, otherwise pass the reference. - match f($(($clone)($par)),*) { - Ok(r) => Ok(Box::new(r) as Dynamic), - Err(mut err) => { - err.set_position(pos); - Err(err) - } - } + f($(($clone)($par)),*).map(|r| Box::new(r) as Dynamic).map_err(|mut err| { + err.set_position(pos); + err + }) }; self.register_fn_raw(name, Some(vec![$(TypeId::of::<$par>()),*]), Box::new(fun)); } From 372321dfe30f98e248f9fa132f67577ec6f9851b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 15 Mar 2020 22:39:58 +0800 Subject: [PATCH 14/21] Add full optimization level for aggressive optimizing. --- Cargo.toml | 2 +- README.md | 66 +++++++-- examples/repl.rs | 7 + examples/rhai_runner.rs | 7 +- src/api.rs | 84 ++++++++--- src/engine.rs | 70 ++++++--- src/lib.rs | 3 + src/optimize.rs | 153 +++++++++++++++---- src/parser.rs | 251 +++++++++++++++++++------------- src/scope.rs | 74 +--------- tests/{engine.rs => call_fn.rs} | 11 +- tests/constants.rs | 8 +- tests/mismatched_op.rs | 16 +- tests/optimizer.rs | 32 ++++ 14 files changed, 512 insertions(+), 272 deletions(-) rename tests/{engine.rs => call_fn.rs} (64%) create mode 100644 tests/optimizer.rs diff --git a/Cargo.toml b/Cargo.toml index 4ed917d9..7eb97fb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ include = [ num-traits = "*" [features] -#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked"] +#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] default = [] debug_msgs = [] # print debug messages on function registrations and calls unchecked = [] # unchecked arithmetic diff --git a/README.md b/README.md index b5870ee1..1a346f23 100644 --- a/README.md +++ b/README.md @@ -869,13 +869,13 @@ Compound assignment operators ```rust let number = 5; -number += 4; // number = number + 4 -number -= 3; // number = number - 3 -number *= 2; // number = number * 2 -number /= 1; // number = number / 1 -number %= 3; // number = number % 3 -number <<= 2; // number = number << 2 -number >>= 1; // number = number >> 1 +number += 4; // number = number + 4 +number -= 3; // number = number - 3 +number *= 2; // number = number * 2 +number /= 1; // number = number / 1 +number %= 3; // number = number % 3 +number <<= 2; // number = number << 2 +number >>= 1; // number = number >> 1 ``` The `+=` operator can also be used to build strings: @@ -1096,8 +1096,8 @@ for entry in log { } ``` -Optimizations -============= +Script optimization +=================== Rhai includes an _optimizer_ that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed. Script optimization can be turned off via the [`no_optimize`] feature. @@ -1167,21 +1167,55 @@ const DECISION_2 = false; const DECISION_3 = false; if DECISION_1 { - : // this branch is kept + : // this branch is kept and promoted to the parent level } else if DECISION_2 { - : // this branch is eliminated + : // this branch is eliminated } else if DECISION_3 { - : // this branch is eliminated + : // this branch is eliminated } else { - : // this branch is eliminated + : // this branch is eliminated } ``` In general, boolean constants are most effective if you want the optimizer to automatically prune large `if`-`else` branches because they do not depend on operators. +Alternatively, turn the optimizer to [`OptimizationLevel::Full`] + Here be dragons! ---------------- +### Optimization levels + +There are actually three levels of optimizations: `None`, `Simple` and `Full`. + +`None` is obvious - no optimization on the AST is performed. + +`Simple` performs relatively _safe_ optimizations without causing side effects (i.e. it only relies on static analysis and will not actually perform any function calls). `Simple` is the default. + +`Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators. + +```rust +// The following run with OptimizationLevel::Full + +const DECISION = 1; + +if DECISION == 1 { // this condition is now eliminated because 'DECISION == 1' is a function call to the '==' function, and it returns 'true' + print("hello!"); // the 'true' block is promoted to the parent level +} else { + print("boo!"); // the 'else' block is eliminated +} + +print("hello!"); // <- the above is equivalent to this +``` + +### Side effect considerations + +All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe. +Beware, however, that if custom functions are registered, they'll also be called. If custom functions are registered to replace built-in operator functions, +the custom functions will be called and _may_ cause side-effects. + +### Subtle semantic changes + Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example: ```rust @@ -1214,12 +1248,14 @@ print("end!"); In the script above, if `my_decision` holds anything other than a boolean value, the script should have been terminated due to a type error. However, after optimization, the entire `if` statement is removed, thus the script silently runs to completion without errors. +### Turning off optimizations + It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess), there is a setting in `Engine` to turn off optimizations. ```rust let engine = rhai::Engine::new(); -engine.set_optimization(false); // turn off the optimizer +engine.set_optimization_level(rhai::OptimizationLevel::None); // turn off the optimizer ``` @@ -1237,3 +1273,5 @@ engine.set_optimization(false); // turn off the optimizer [`Engine`]: #hello-world [`Scope`]: #initializing-and-maintaining-state [`Dynamic`]: #values-and-types + +[`OptimizationLevel::Full`]: #optimization-levels diff --git a/examples/repl.rs b/examples/repl.rs index 68ae0c96..76bdcd48 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -1,4 +1,7 @@ +#[cfg(not(feature = "no_optimize"))] +use rhai::OptimizationLevel; use rhai::{Engine, EvalAltResult, Scope, AST}; + use std::{ io::{stdin, stdout, Write}, iter, @@ -43,6 +46,10 @@ fn print_error(input: &str, err: EvalAltResult) { fn main() { let mut engine = Engine::new(); + + #[cfg(not(feature = "no_optimize"))] + engine.set_optimization_level(OptimizationLevel::Full); + let mut scope = Scope::new(); let mut input = String::new(); diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 2955c76e..59fe6770 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -1,3 +1,5 @@ +#[cfg(not(feature = "no_optimize"))] +use rhai::OptimizationLevel; use rhai::{Engine, EvalAltResult}; use std::{env, fs::File, io::Read, iter, process::exit}; @@ -49,6 +51,9 @@ fn main() { for filename in env::args().skip(1) { let mut engine = Engine::new(); + #[cfg(not(feature = "no_optimize"))] + engine.set_optimization_level(OptimizationLevel::Full); + let mut f = match File::open(&filename) { Err(err) => { eprintln!("Error reading script file: {}\n{}", filename, err); @@ -67,7 +72,7 @@ fn main() { _ => (), } - if let Err(err) = engine.consume(&contents, false) { + if let Err(err) = engine.consume(false, &contents) { eprintln!("{}", padding("=", filename.len())); eprintln!("{}", filename); eprintln!("{}", padding("=", filename.len())); diff --git a/src/api.rs b/src/api.rs index ff57c51c..3c248d5b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -8,6 +8,10 @@ use crate::fn_register::RegisterFn; use crate::parser::{lex, parse, FnDef, Position, AST}; use crate::result::EvalAltResult; use crate::scope::Scope; + +#[cfg(not(feature = "no_optimize"))] +use crate::optimize::optimize_ast; + use std::{ any::{type_name, TypeId}, fs::File, @@ -108,7 +112,7 @@ impl<'e> Engine<'e> { /// 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(), scope, self.optimize) + parse(&mut tokens_stream.peekable(), self, scope) } fn read_file(path: PathBuf) -> Result { @@ -145,6 +149,15 @@ impl<'e> Engine<'e> { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } + /// Evaluate a file with own scope. + pub fn eval_file_with_scope( + &mut self, + scope: &mut Scope, + path: PathBuf, + ) -> Result { + Self::read_file(path).and_then(|contents| self.eval_with_scope::(scope, &contents)) + } + /// Evaluate a string. pub fn eval(&mut self, input: &str) -> Result { let mut scope = Scope::new(); @@ -180,10 +193,6 @@ impl<'e> Engine<'e> { ) -> Result { engine.clear_functions(); - #[cfg(feature = "no_function")] - let AST(statements) = ast; - - #[cfg(not(feature = "no_function"))] let statements = { let AST(statements, functions) = ast; engine.load_script_functions(functions); @@ -227,10 +236,25 @@ impl<'e> Engine<'e> { /// and not cleared from run to run. pub fn consume_file( &mut self, - path: PathBuf, retain_functions: bool, + path: PathBuf, ) -> Result<(), EvalAltResult> { - Self::read_file(path).and_then(|contents| self.consume(&contents, retain_functions)) + Self::read_file(path).and_then(|contents| self.consume(retain_functions, &contents)) + } + + /// Evaluate a file with own scope, but throw away the result and only return error (if any). + /// Useful for when you don't need the result, but still need to keep track of possible errors. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. + pub fn consume_file_with_scope( + &mut self, + scope: &mut Scope, + retain_functions: bool, + path: PathBuf, + ) -> Result<(), EvalAltResult> { + Self::read_file(path) + .and_then(|contents| self.consume_with_scope(scope, retain_functions, &contents)) } /// Evaluate a string, but throw away the result and only return error (if any). @@ -238,11 +262,11 @@ impl<'e> Engine<'e> { /// /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ /// and not cleared from run to run. - pub fn consume(&mut self, input: &str, retain_functions: bool) -> Result<(), EvalAltResult> { + pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> { self.consume_with_scope(&mut Scope::new(), retain_functions, input) } - /// Evaluate a string, but throw away the result and only return error (if any). + /// Evaluate a string with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ @@ -255,7 +279,7 @@ impl<'e> Engine<'e> { ) -> Result<(), EvalAltResult> { let tokens_stream = lex(input); - let ast = parse(&mut tokens_stream.peekable(), scope, self.optimize) + let ast = parse(&mut tokens_stream.peekable(), self, scope) .map_err(EvalAltResult::ErrorParsing)?; self.consume_ast_with_scope(scope, retain_functions, &ast) @@ -266,6 +290,15 @@ impl<'e> Engine<'e> { /// /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ /// and not cleared from run to run. + pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> { + self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast) + } + + /// Evaluate an AST with own scope, but throw away the result and only return error (if any). + /// Useful for when you don't need the result, but still need to keep track of possible errors. + /// + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn consume_ast_with_scope( &mut self, scope: &mut Scope, @@ -276,10 +309,6 @@ impl<'e> Engine<'e> { self.clear_functions(); } - #[cfg(feature = "no_function")] - let AST(statements) = ast; - - #[cfg(not(feature = "no_function"))] let statements = { let AST(ref statements, ref functions) = ast; self.load_script_functions(functions); @@ -327,7 +356,7 @@ impl<'e> Engine<'e> { /// /// let mut engine = Engine::new(); /// - /// engine.consume("fn add(x, y) { x.len() + y }", true)?; + /// engine.consume(true, "fn add(x, y) { x.len() + y }")?; /// /// let result: i64 = engine.call_fn("add", (String::from("abc"), 123_i64))?; /// @@ -365,6 +394,27 @@ impl<'e> Engine<'e> { }) } + /// Optimize the AST with constants defined in an external Scope. + /// An optimized copy of the AST is returned while the original AST is untouched. + /// + /// 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 recompile a large script. The script AST can be + /// compiled just once. Before 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. + #[cfg(not(feature = "no_optimize"))] + pub fn optimize_ast(&self, scope: &Scope, ast: &AST) -> AST { + optimize_ast( + self, + scope, + ast.0.clone(), + ast.1.iter().map(|f| (**f).clone()).collect(), + ) + } + /// Override default action of `print` (print to stdout using `println!`) /// /// # Example @@ -379,7 +429,7 @@ impl<'e> Engine<'e> { /// /// // Override action of 'print' function /// engine.on_print(|s| result.push_str(s)); - /// engine.consume("print(40 + 2);", false)?; + /// engine.consume(false, "print(40 + 2);")?; /// } /// assert_eq!(result, "42"); /// # Ok(()) @@ -403,7 +453,7 @@ impl<'e> Engine<'e> { /// /// // Override action of 'debug' function /// engine.on_debug(|s| result.push_str(s)); - /// engine.consume(r#"debug("hello");"#, false)?; + /// engine.consume(false, r#"debug("hello");"#)?; /// } /// assert_eq!(result, "\"hello\""); /// # Ok(()) diff --git a/src/engine.rs b/src/engine.rs index f1d9eecd..6b951d4e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -5,6 +5,9 @@ use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt}; use crate::result::EvalAltResult; use crate::scope::{Scope, VariableType}; +#[cfg(not(feature = "no_optimize"))] +use crate::optimize::OptimizationLevel; + #[cfg(not(feature = "no_index"))] use crate::INT; @@ -63,17 +66,20 @@ pub struct FnSpec<'a> { /// ``` pub struct Engine<'e> { /// Optimize the AST after compilation - pub(crate) optimize: bool, + #[cfg(not(feature = "no_optimize"))] + pub(crate) optimization_level: OptimizationLevel, /// A hashmap containing all compiled functions known to the engine pub(crate) ext_functions: HashMap, Box>, /// A hashmap containing all script-defined functions pub(crate) script_functions: Vec>, /// A hashmap containing all iterators known to the engine pub(crate) type_iterators: HashMap>, + /// A hashmap mapping type names to pretty-print names pub(crate) type_names: HashMap, - // Closures for implementing the print/debug commands + /// Closure for implementing the print commands pub(crate) on_print: Box, + /// Closure for implementing the debug commands pub(crate) on_debug: Box, } @@ -93,7 +99,8 @@ impl Engine<'_> { // Create the new scripting Engine let mut engine = Engine { - optimize: true, + #[cfg(not(feature = "no_optimize"))] + optimization_level: OptimizationLevel::Full, ext_functions: HashMap::new(), script_functions: Vec::new(), type_iterators: HashMap::new(), @@ -110,9 +117,32 @@ impl Engine<'_> { engine } - /// Control whether the `Engine` will optimize an AST after compilation - pub fn set_optimization(&mut self, optimize: bool) { - self.optimize = optimize + /// Control whether and how the `Engine` will optimize an AST after compilation + #[cfg(not(feature = "no_optimize"))] + pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) { + self.optimization_level = optimization_level + } + + /// Call a registered function + #[cfg(not(feature = "no_optimize"))] + pub(crate) fn call_ext_fn_raw( + &self, + fn_name: &str, + args: FnCallArgs, + pos: Position, + ) -> Result, EvalAltResult> { + let spec = FnSpec { + name: fn_name.into(), + args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()), + }; + + // Search built-in's and external functions + if let Some(func) = self.ext_functions.get(&spec) { + // Run external function + Ok(Some(func(args, pos)?)) + } else { + Ok(None) + } } /// Universal method for calling functions, that are either @@ -165,13 +195,13 @@ impl Engine<'_> { args: Some(args.iter().map(|a| Any::type_id(&**a)).collect()), }; - // Then search built-in's and external functions + // Search built-in's and external functions if let Some(func) = self.ext_functions.get(&spec) { // Run external function let result = func(args, pos)?; // See if the function match print/debug (which requires special processing) - let callback = match spec.name.as_ref() { + let callback = match fn_name { KEYWORD_PRINT => self.on_print.as_mut(), KEYWORD_DEBUG => self.on_debug.as_mut(), _ => return Ok(result), @@ -185,7 +215,7 @@ impl Engine<'_> { return Ok(callback(val).into_dynamic()); } - if spec.name == KEYWORD_TYPE_OF && args.len() == 1 { + if fn_name == KEYWORD_TYPE_OF && args.len() == 1 { // Handle `type_of` function return Ok(self .map_type_name(args[0].type_name()) @@ -193,23 +223,23 @@ impl Engine<'_> { .into_dynamic()); } - if spec.name.starts_with(FUNC_GETTER) { + if fn_name.starts_with(FUNC_GETTER) { // Getter function not found return Err(EvalAltResult::ErrorDotExpr( format!( "- property '{}' unknown or write-only", - &spec.name[FUNC_GETTER.len()..] + &fn_name[FUNC_GETTER.len()..] ), pos, )); } - if spec.name.starts_with(FUNC_SETTER) { + if fn_name.starts_with(FUNC_SETTER) { // Setter function not found return Err(EvalAltResult::ErrorDotExpr( format!( "- property '{}' unknown or read-only", - &spec.name[FUNC_SETTER.len()..] + &fn_name[FUNC_SETTER.len()..] ), pos, )); @@ -228,7 +258,7 @@ impl Engine<'_> { .collect::>(); Err(EvalAltResult::ErrorFunctionNotFound( - format!("{} ({})", spec.name, types_list.join(", ")), + format!("{} ({})", fn_name, types_list.join(", ")), pos, )) } @@ -249,7 +279,7 @@ impl Engine<'_> { .collect::, _>>()?; let args = once(this_ptr) - .chain(values.iter_mut().map(|b| b.as_mut())) + .chain(values.iter_mut().map(Dynamic::as_mut)) .collect(); self.call_fn_raw(fn_name, args, def_val.as_ref(), *pos) @@ -567,8 +597,7 @@ impl Engine<'_> { Ok(Self::str_replace_char(s, idx as usize, ch).into_dynamic()) } - // All other variable types should be an error - _ => panic!("array or string source type expected for indexing"), + IndexSourceType::Expression => panic!("expression cannot be indexed for update"), } } @@ -809,9 +838,6 @@ impl Engine<'_> { .eval_index_expr(scope, lhs, idx_expr, *idx_pos) .map(|(_, _, _, x)| x), - #[cfg(feature = "no_index")] - Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"), - // Statement block Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt), @@ -870,7 +896,7 @@ impl Engine<'_> { // Error assignment to constant expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant( - expr.get_value_str(), + expr.get_constant_str(), lhs.position(), )), @@ -891,8 +917,6 @@ impl Engine<'_> { Ok(Box::new(arr)) } - #[cfg(feature = "no_index")] - Expr::Array(_, _) => panic!("encountered an array during no_index!"), // Dump AST Expr::FunctionCall(fn_name, args_expr_list, _, pos) if fn_name == KEYWORD_DUMP_AST => { diff --git a/src/lib.rs b/src/lib.rs index 47be664d..ceb34396 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,3 +87,6 @@ pub use engine::Array; #[cfg(not(feature = "no_float"))] pub use parser::FLOAT; + +#[cfg(not(feature = "no_optimize"))] +pub use optimize::OptimizationLevel; diff --git a/src/optimize.rs b/src/optimize.rs index 6013ea01..cadadd59 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,21 +1,41 @@ #![cfg(not(feature = "no_optimize"))] -use crate::engine::KEYWORD_DUMP_AST; -use crate::parser::{Expr, Stmt}; +use crate::any::Dynamic; +use crate::engine::{Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT}; +use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST}; use crate::scope::{Scope, ScopeEntry, VariableType}; -struct State { - changed: bool, - constants: Vec<(String, Expr)>, +use std::sync::Arc; + +/// Level of optimization performed +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] +pub enum OptimizationLevel { + /// No optimization performed + None, + /// Only perform simple optimizations without evaluating functions + Simple, + /// Full optimizations performed, including evaluating functions. + /// Take care that this may cause side effects. + Full, } -impl State { +struct State<'a> { + changed: bool, + constants: Vec<(String, Expr)>, + engine: Option<&'a Engine<'a>>, +} + +impl State<'_> { pub fn new() -> Self { State { changed: false, constants: vec![], + engine: None, } } + pub fn reset(&mut self) { + self.changed = false; + } pub fn set_dirty(&mut self) { self.changed = true; } @@ -42,7 +62,7 @@ impl State { } } -fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { +fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt { match stmt { Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => { state.set_dirty(); @@ -114,7 +134,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { Stmt::Block(statements, pos) => { let orig_len = statements.len(); - let orig_constants = state.constants.len(); + let orig_constants_len = state.constants.len(); let mut result: Vec<_> = statements .into_iter() // For each statement @@ -175,7 +195,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { state.set_dirty(); } - state.restore_constants(orig_constants); + state.restore_constants(orig_constants_len); match result[..] { // No statements in block - change to No-op @@ -202,7 +222,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { } } -fn optimize_expr(expr: Expr, state: &mut State) -> Expr { +fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { match expr { Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) { Stmt::Noop(_) => { @@ -261,8 +281,6 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { pos, ), }, - #[cfg(feature = "no_index")] - Expr::Index(_, _, _) => panic!("encountered an index expression during no_index!"), #[cfg(not(feature = "no_index"))] Expr::Array(items, pos) => { @@ -280,9 +298,6 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { Expr::Array(items, pos) } - #[cfg(feature = "no_index")] - Expr::Array(_, _) => panic!("encountered an array during no_index!"), - Expr::And(lhs, rhs) => match (*lhs, *rhs) { (Expr::True(_), rhs) => { state.set_dirty(); @@ -320,9 +335,33 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { ), }, + // Do not optimize anything within `dump_ast` Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST => { Expr::FunctionCall(id, args, def_value, pos) } + // Actually call function to optimize it + Expr::FunctionCall(id, args, def_value, pos) + if id != KEYWORD_DEBUG // not debug + && id != KEYWORD_PRINT // not print + && state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations + && args.iter().all(|expr| expr.is_constant()) // all arguments are constants + => + { + let engine = state.engine.expect("engine should be Some"); + let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); + let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect(); + if let Ok(r) = engine.call_ext_fn_raw(&id, call_args, pos) { + r.and_then(|result| map_dynamic_to_expr(result, pos).0) + .map(|expr| { + state.set_dirty(); + expr + }) + .unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) + } else { + Expr::FunctionCall(id, args, def_value, pos) + } + } + // Optimize the function call arguments Expr::FunctionCall(id, args, def_value, pos) => { let orig_len = args.len(); @@ -341,7 +380,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // Replace constant with value state .find_constant(name) - .expect("can't find constant in scope!") + .expect("should find constant in scope!") .clone() } @@ -349,26 +388,47 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } } -pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { +pub(crate) fn optimize<'a>( + statements: Vec, + engine: Option<&Engine<'a>>, + scope: &Scope, +) -> Vec { + // If optimization level is None then skip optimizing + if engine + .map(|eng| eng.optimization_level == OptimizationLevel::None) + .unwrap_or(false) + { + return statements; + } + + // Set up the state + let mut state = State::new(); + state.engine = engine; + + scope + .iter() + .filter(|ScopeEntry { var_type, expr, .. }| { + // Get all the constants with definite constant expressions + *var_type == VariableType::Constant + && expr.as_ref().map(Expr::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(), + ) + }); + + let orig_constants_len = state.constants.len(); + + // Optimization loop let mut result = statements; loop { - let mut state = State::new(); - let num_statements = result.len(); + state.reset(); + state.restore_constants(orig_constants_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(), - ) - }); + let num_statements = result.len(); result = result .into_iter() @@ -405,3 +465,32 @@ pub(crate) fn optimize(statements: Vec, scope: &Scope) -> Vec { result } + +pub fn optimize_ast( + engine: &Engine, + scope: &Scope, + statements: Vec, + functions: Vec, +) -> AST { + AST( + match engine.optimization_level { + OptimizationLevel::None => statements, + OptimizationLevel::Simple => optimize(statements, None, &scope), + OptimizationLevel::Full => optimize(statements, Some(engine), &scope), + }, + functions + .into_iter() + .map(|mut fn_def| { + match engine.optimization_level { + OptimizationLevel::None => (), + OptimizationLevel::Simple | OptimizationLevel::Full => { + let pos = fn_def.body.position(); + let mut body = optimize(vec![fn_def.body], None, &Scope::new()); + fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); + } + } + Arc::new(fn_def) + }) + .collect(), + ) +} diff --git a/src/parser.rs b/src/parser.rs index e1b8d9da..d87d351b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,11 +1,12 @@ //! Main module defining the lexer and parser. -use crate::any::Dynamic; +use crate::any::{Any, AnyExt, Dynamic}; +use crate::engine::Engine; use crate::error::{LexError, ParseError, ParseErrorType}; use crate::scope::{Scope, VariableType}; #[cfg(not(feature = "no_optimize"))] -use crate::optimize::optimize; +use crate::optimize::optimize_ast; use std::{ borrow::Cow, char, cmp::Ordering, fmt, iter::Peekable, str::Chars, str::FromStr, sync::Arc, @@ -147,49 +148,9 @@ impl fmt::Debug for Position { /// Compiled AST (abstract syntax tree) of a Rhai script. #[derive(Debug, Clone)] -pub struct AST( - pub(crate) Vec, - #[cfg(not(feature = "no_function"))] pub(crate) Vec>, -); +pub struct AST(pub(crate) Vec, 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 just once. During actual 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. - #[cfg(not(feature = "no_optimize"))] - pub fn optimize(self, scope: &Scope) -> Self { - AST( - crate::optimize::optimize(self.0, scope), - #[cfg(not(feature = "no_function"))] - self.1 - .into_iter() - .map(|fn_def| { - let pos = fn_def.body.position(); - let body = optimize(vec![fn_def.body.clone()], scope) - .into_iter() - .next() - .unwrap_or_else(|| Stmt::Noop(pos)); - Arc::new(FnDef { - name: fn_def.name.clone(), - params: fn_def.params.clone(), - body, - pos: fn_def.pos, - }) - }) - .collect(), - ) - } -} - -#[derive(Debug)] // Do not derive Clone because it is expensive +#[derive(Debug, Clone)] pub struct FnDef { pub name: String, pub params: Vec, @@ -267,7 +228,9 @@ pub enum Expr { FunctionCall(String, Vec, Option, Position), Assignment(Box, Box, Position), Dot(Box, Box, Position), + #[cfg(not(feature = "no_index"))] Index(Box, Box, Position), + #[cfg(not(feature = "no_index"))] Array(Vec, Position), And(Box, Box), Or(Box, Box), @@ -277,7 +240,30 @@ pub enum Expr { } impl Expr { - pub fn get_value_str(&self) -> String { + pub fn get_constant_value(&self) -> Dynamic { + match self { + Expr::IntegerConstant(i, _) => i.into_dynamic(), + Expr::CharConstant(c, _) => c.into_dynamic(), + Expr::StringConstant(s, _) => s.into_dynamic(), + Expr::True(_) => true.into_dynamic(), + Expr::False(_) => false.into_dynamic(), + Expr::Unit(_) => ().into_dynamic(), + + #[cfg(not(feature = "no_index"))] + Expr::Array(items, _) if items.iter().all(Expr::is_constant) => items + .iter() + .map(Expr::get_constant_value) + .collect::>() + .into_dynamic(), + + #[cfg(not(feature = "no_float"))] + Expr::FloatConstant(f, _) => f.into_dynamic(), + + _ => panic!("cannot get value of non-constant expression"), + } + } + + pub fn get_constant_str(&self) -> String { match self { Expr::IntegerConstant(i, _) => i.to_string(), Expr::CharConstant(c, _) => c.to_string(), @@ -286,10 +272,13 @@ impl Expr { Expr::False(_) => "false".to_string(), Expr::Unit(_) => "()".to_string(), + #[cfg(not(feature = "no_index"))] + Expr::Array(items, _) if items.iter().all(Expr::is_constant) => "array".to_string(), + #[cfg(not(feature = "no_float"))] Expr::FloatConstant(f, _) => f.to_string(), - _ => "".to_string(), + _ => panic!("cannot get value of non-constant expression"), } } @@ -302,19 +291,22 @@ impl Expr { | Expr::Property(_, pos) | Expr::Stmt(_, pos) | Expr::FunctionCall(_, _, _, pos) - | Expr::Array(_, pos) | Expr::True(pos) | Expr::False(pos) | Expr::Unit(pos) => *pos, - Expr::Assignment(e, _, _) - | Expr::Dot(e, _, _) - | Expr::Index(e, _, _) - | Expr::And(e, _) - | Expr::Or(e, _) => e.position(), + Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => { + e.position() + } #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => *pos, + + #[cfg(not(feature = "no_index"))] + Expr::Array(_, pos) => *pos, + + #[cfg(not(feature = "no_index"))] + Expr::Index(e, _, _) => e.position(), } } @@ -323,8 +315,14 @@ impl Expr { /// A pure expression has no side effects. pub fn is_pure(&self) -> bool { match self { + #[cfg(not(feature = "no_index"))] 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(), + + #[cfg(not(feature = "no_index"))] + Expr::Index(x, y, _) => x.is_pure() && y.is_pure(), + + Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(), + expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)), } } @@ -338,11 +336,12 @@ impl Expr { | Expr::False(_) | Expr::Unit(_) => true, - Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant), - #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, _) => true, + #[cfg(not(feature = "no_index"))] + Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant), + _ => false, } } @@ -360,7 +359,9 @@ pub enum Token { RightBrace, LeftParen, RightParen, + #[cfg(not(feature = "no_index"))] LeftBracket, + #[cfg(not(feature = "no_index"))] RightBracket, Plus, UnaryPlus, @@ -392,6 +393,7 @@ pub enum Token { Or, Ampersand, And, + #[cfg(not(feature = "no_function"))] Fn, Break, Return, @@ -435,7 +437,9 @@ impl Token { RightBrace => "}", LeftParen => "(", RightParen => ")", + #[cfg(not(feature = "no_index"))] LeftBracket => "[", + #[cfg(not(feature = "no_index"))] RightBracket => "]", Plus => "+", UnaryPlus => "+", @@ -467,6 +471,7 @@ impl Token { Or => "||", Ampersand => "&", And => "&&", + #[cfg(not(feature = "no_function"))] Fn => "fn", Break => "break", Return => "return", @@ -506,8 +511,6 @@ impl Token { // RightBrace | {expr} - expr not unary & is closing LeftParen | // {-expr} - is unary // RightParen | (expr) - expr not unary & is closing - LeftBracket | // [-expr] - is unary - // RightBracket | [expr] - expr not unary & is closing Plus | UnaryPlus | Minus | @@ -551,6 +554,10 @@ impl Token { In | PowerOfAssign => true, + #[cfg(not(feature = "no_index"))] + LeftBracket => true, // [-expr] - is unary + // RightBracket | [expr] - expr not unary & is closing + _ => false, } } @@ -560,9 +567,12 @@ impl Token { use self::Token::*; match *self { - RightBrace | RightParen | RightBracket | Plus | Minus | Multiply | Divide | Comma - | Equals | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo - | EqualsTo | NotEqualsTo | Pipe | Or | Ampersand | And | PowerOf => true, + RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan + | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo + | Pipe | Or | Ampersand | And | PowerOf => true, + + #[cfg(not(feature = "no_index"))] + RightBrace | RightBracket => true, _ => false, } @@ -887,9 +897,12 @@ impl<'a> TokenIterator<'a> { "break" => Token::Break, "return" => Token::Return, "throw" => Token::Throw, - "fn" => Token::Fn, "for" => Token::For, "in" => Token::In, + + #[cfg(not(feature = "no_function"))] + "fn" => Token::Fn, + _ => Token::Identifier(out), }, pos, @@ -924,8 +937,12 @@ impl<'a> TokenIterator<'a> { '}' => return Some((Token::RightBrace, pos)), '(' => return Some((Token::LeftParen, pos)), ')' => return Some((Token::RightParen, pos)), + + #[cfg(not(feature = "no_index"))] '[' => return Some((Token::LeftBracket, pos)), + #[cfg(not(feature = "no_index"))] ']' => return Some((Token::RightBracket, pos)), + '+' => { return Some(( match self.char_stream.peek() { @@ -1745,6 +1762,7 @@ fn parse_binary_op<'a>( Box::new(change_var_to_property(*rhs)), pos, ), + #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx, pos) => { Expr::Index(Box::new(change_var_to_property(*lhs)), idx, pos) } @@ -1950,6 +1968,8 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result (), // empty block + + #[cfg(not(feature = "no_function"))] Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)), _ => { @@ -2003,7 +2023,7 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result ReturnType::Return, Token::Throw => ReturnType::Exception, - _ => panic!("unexpected token!"), + _ => panic!("token should be return or throw"), }; input.next(); @@ -2095,14 +2115,10 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( +fn parse_top_level<'a, 'e>( input: &mut Peekable>, - scope: &Scope, - optimize_ast: bool, -) -> Result { +) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); - - #[cfg(not(feature = "no_function"))] let mut functions = Vec::::new(); while input.peek().is_some() { @@ -2126,40 +2142,79 @@ fn parse_top_level<'a>( } } + Ok((statements, functions)) +} + +pub fn parse<'a, 'e>( + input: &mut Peekable>, + engine: &Engine<'e>, + scope: &Scope, +) -> Result { + let (statements, functions) = parse_top_level(input)?; + Ok( #[cfg(not(feature = "no_optimize"))] - AST( - if optimize_ast { - optimize(statements, &scope) - } else { - statements - }, - #[cfg(not(feature = "no_function"))] - functions - .into_iter() - .map(|mut fn_def| { - if optimize_ast { - let pos = fn_def.body.position(); - let mut body = optimize(vec![fn_def.body], &scope); - fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); - } - Arc::new(fn_def) - }) - .collect(), - ), + optimize_ast(engine, scope, statements, functions), #[cfg(feature = "no_optimize")] - AST( - statements, - #[cfg(not(feature = "no_function"))] - functions.into_iter().map(Arc::new).collect(), - ), + AST(statements, functions.into_iter().map(Arc::new).collect()), ) } -pub fn parse<'a>( - input: &mut Peekable>, - scope: &Scope, - optimize_ast: bool, -) -> Result { - parse_top_level(input, scope, optimize_ast) +pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option, Dynamic) { + if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::IntegerConstant( + *value.downcast::().expect("value should be INT"), + pos, + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::CharConstant( + *value.downcast::().expect("value should be char"), + pos, + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some(Expr::StringConstant( + *value.downcast::().expect("value should be String"), + pos, + )), + value2, + ) + } else if value.is::() { + let value2 = value.clone(); + ( + Some( + if *value.downcast::().expect("value should be bool") { + Expr::True(pos) + } else { + Expr::False(pos) + }, + ), + value2, + ) + } else { + #[cfg(not(feature = "no_float"))] + { + if value.is::() { + let value2 = value.clone(); + return ( + Some(Expr::FloatConstant( + *value.downcast::().expect("value should be FLOAT"), + pos, + )), + value2, + ); + } + } + + (None, value) + } } diff --git a/src/scope.rs b/src/scope.rs index b20eb241..92c1fe14 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,10 +1,7 @@ //! Module that defines the `Scope` type representing a function call-stack scope. -use crate::any::{Any, AnyExt, Dynamic}; -use crate::parser::{Expr, Position, INT}; - -#[cfg(not(feature = "no_float"))] -use crate::parser::FLOAT; +use crate::any::{Any, Dynamic}; +use crate::parser::{map_dynamic_to_expr, Expr, Position}; use std::borrow::Cow; @@ -73,7 +70,7 @@ impl<'a> Scope<'a> { let value = value.into_dynamic(); // Map into constant expressions - //let (expr, value) = map_dynamic_to_expr(value); + //let (expr, value) = map_dynamic_to_expr(value, Position::none()); self.0.push(ScopeEntry { name: name.into(), @@ -93,7 +90,7 @@ impl<'a> Scope<'a> { let value = value.into_dynamic(); // Map into constant expressions - let (expr, value) = map_dynamic_to_expr(value); + let (expr, value) = map_dynamic_to_expr(value, Position::none()); self.0.push(ScopeEntry { name: name.into(), @@ -110,13 +107,13 @@ impl<'a> Scope<'a> { var_type: VariableType, value: Dynamic, ) { - //let (expr, value) = map_dynamic_to_expr(value); + let (expr, value) = map_dynamic_to_expr(value, Position::none()); self.0.push(ScopeEntry { name: name.into(), var_type, value, - expr: None, + expr, }); } @@ -210,62 +207,3 @@ where })); } } - -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::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 { - #[cfg(not(feature = "no_float"))] - { - if value.is::() { - let value2 = value.clone(); - return ( - Some(Expr::FloatConstant( - *value.downcast::().expect("value should be FLOAT"), - Position::none(), - )), - value2, - ); - } - } - - (None, value) - } -} diff --git a/tests/engine.rs b/tests/call_fn.rs similarity index 64% rename from tests/engine.rs rename to tests/call_fn.rs index cd9f7fb0..ca6303d6 100644 --- a/tests/engine.rs +++ b/tests/call_fn.rs @@ -1,25 +1,24 @@ -#![cfg(not(feature = "no_stdlib"))] #![cfg(not(feature = "no_function"))] use rhai::{Engine, EvalAltResult, INT}; #[test] -fn test_engine_call_fn() -> Result<(), EvalAltResult> { +fn test_call_fn() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); engine.consume( + true, r" fn hello(x, y) { - x.len() + y + x + y } fn hello(x) { x * 2 } ", - true, )?; - let r: i64 = engine.call_fn("hello", (String::from("abc"), 123 as INT))?; - assert_eq!(r, 126); + let r: i64 = engine.call_fn("hello", (42 as INT, 123 as INT))?; + assert_eq!(r, 165); let r: i64 = engine.call_fn("hello", 123 as INT)?; assert_eq!(r, 246); diff --git a/tests/constants.rs b/tests/constants.rs index 9a93c119..16fbf7bd 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -1,19 +1,19 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_constant() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - assert_eq!(engine.eval::("const x = 123; x")?, 123); + assert_eq!(engine.eval::("const x = 123; x")?, 123); assert!( - matches!(engine.eval::("const x = 123; x = 42;").expect_err("expects error"), + matches!(engine.eval::("const x = 123; x = 42;").expect_err("expects error"), EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x") ); #[cfg(not(feature = "no_index"))] assert!( - matches!(engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), + matches!(engine.eval::("const x = [1, 2, 3, 4, 5]; x[2] = 42;").expect_err("expects error"), EvalAltResult::ErrorAssignmentToConstant(var, _) if var == "x") ); diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 861ab4c4..a3779705 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -32,13 +32,13 @@ fn test_mismatched_op_custom_type() { .eval::("60 + new_ts()") .expect_err("expects error"); - match r { - #[cfg(feature = "only_i32")] - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)" => (), + #[cfg(feature = "only_i32")] + assert!( + matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)") + ); - #[cfg(not(feature = "only_i32"))] - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)" => (), - - _ => panic!(), - } + #[cfg(not(feature = "only_i32"))] + assert!( + matches!(r, EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)") + ); } diff --git a/tests/optimizer.rs b/tests/optimizer.rs new file mode 100644 index 00000000..eb9f1d5c --- /dev/null +++ b/tests/optimizer.rs @@ -0,0 +1,32 @@ +#![cfg(not(feature = "no_optimize"))] + +use rhai::{Engine, EvalAltResult, OptimizationLevel, INT}; + +#[test] +fn test_optimizer() -> Result<(), EvalAltResult> { + fn run_test(engine: &mut Engine) -> Result<(), EvalAltResult> { + assert_eq!(engine.eval::(r"if true { 42 } else { 123 }")?, 42); + assert_eq!( + engine.eval::(r"if 1 == 1 || 2 > 3 { 42 } else { 123 }")?, + 42 + ); + assert_eq!( + engine.eval::(r#"const abc = "hello"; if abc < "foo" { 42 } else { 123 }"#)?, + 123 + ); + Ok(()) + } + + let mut engine = Engine::new(); + + engine.set_optimization_level(OptimizationLevel::None); + run_test(&mut engine)?; + + engine.set_optimization_level(OptimizationLevel::Simple); + run_test(&mut engine)?; + + engine.set_optimization_level(OptimizationLevel::Full); + run_test(&mut engine)?; + + Ok(()) +} From 2c90fea76487dfab6c394a32a4baa83028f2080a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 12:38:01 +0800 Subject: [PATCH 15/21] Catch illegal variable names. --- src/error.rs | 46 +++++++++++++++++++++++++++++++++++++--------- src/parser.rs | 29 +++++++++++++++++++---------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/error.rs b/src/error.rs index 89d6f370..a582bcb2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,8 @@ pub enum LexError { MalformedChar(String), /// Error in the script text. InputError(String), + /// An identifier is in an invalid format. + MalformedIdentifier(String), } impl Error for LexError {} @@ -29,6 +31,9 @@ impl fmt::Display for LexError { Self::MalformedEscapeSequence(s) => write!(f, "Invalid escape sequence: '{}'", s), Self::MalformedNumber(s) => write!(f, "Invalid number: '{}'", s), Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s), + Self::MalformedIdentifier(s) => { + write!(f, "Variable name is not in a legal format: '{}'", s) + } Self::InputError(s) => write!(f, "{}", s), Self::UnterminatedString => write!(f, "Open string is not terminated"), } @@ -51,20 +56,27 @@ pub enum ParseErrorType { /// An open `{` is missing the corresponding closing `}`. MissingRightBrace(String), /// An open `[` is missing the corresponding closing `]`. + #[cfg(not(feature = "no_index"))] MissingRightBracket(String), /// An expression in function call arguments `()` has syntax error. MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. + #[cfg(not(feature = "no_index"))] MalformedIndexExpr(String), /// Invalid expression assigned to constant. ForbiddenConstantExpr(String), - /// Missing a variable name after the `let` keyword. - VarExpectsIdentifier, + /// Missing a variable name after the `let`, `const` or `for` keywords. + VariableExpected, + /// A `for` statement is missing the `in` keyword. + MissingIn, /// Defining a function `fn` in an appropriate place (e.g. inside another function). + #[cfg(not(feature = "no_function"))] WrongFnDefinition, /// Missing a function name after the `fn` keyword. + #[cfg(not(feature = "no_function"))] FnMissingName, /// A function definition is missing the parameters list. Wrapped value is the function name. + #[cfg(not(feature = "no_function"))] FnMissingParams(String), /// Assignment to an inappropriate LHS (left-hand-side) expression. AssignmentToInvalidLHS, @@ -102,13 +114,19 @@ impl ParseError { ParseErrorType::MissingRightParen(_) => "Expecting ')'", ParseErrorType::MissingLeftBrace => "Expecting '{'", ParseErrorType::MissingRightBrace(_) => "Expecting '}'", + #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(_) => "Expecting ']'", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", + #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant", - ParseErrorType::VarExpectsIdentifier => "Expecting name of a variable", + ParseErrorType::MissingIn => "Expecting 'in'", + ParseErrorType::VariableExpected => "Expecting name of a variable", + #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingName => "Expecting name in function declaration", + #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", + #[cfg(not(feature = "no_function"))] ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression", ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value", @@ -122,21 +140,31 @@ impl Error for ParseError {} impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { - ParseErrorType::BadInput(ref s) - | ParseErrorType::MalformedIndexExpr(ref s) - | ParseErrorType::MalformedCallExpr(ref s) => { + ParseErrorType::BadInput(ref s) | ParseErrorType::MalformedCallExpr(ref s) => { write!(f, "{}", if s.is_empty() { self.desc() } else { s })? } ParseErrorType::ForbiddenConstantExpr(ref s) => { write!(f, "Expecting a constant to assign to '{}'", s)? } ParseErrorType::UnknownOperator(ref s) => write!(f, "{}: '{}'", self.desc(), s)?, + + #[cfg(not(feature = "no_index"))] + ParseErrorType::MalformedIndexExpr(ref s) => { + write!(f, "{}", if s.is_empty() { self.desc() } else { s })? + } + + #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(ref s) => { write!(f, "Expecting parameters for function '{}'", s)? } - ParseErrorType::MissingRightParen(ref s) - | ParseErrorType::MissingRightBrace(ref s) - | ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, + + ParseErrorType::MissingRightParen(ref s) | ParseErrorType::MissingRightBrace(ref s) => { + write!(f, "{} for {}", self.desc(), s)? + } + + #[cfg(not(feature = "no_index"))] + ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { write!(f, "{}", self.desc())? } diff --git a/src/parser.rs b/src/parser.rs index d87d351b..366329c3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -882,10 +882,11 @@ impl<'a> TokenIterator<'a> { } } - let out: String = result.iter().collect(); + let has_letter = result.iter().any(char::is_ascii_alphabetic); + let identifier: String = result.iter().collect(); return Some(( - match out.as_str() { + match identifier.as_str() { "true" => Token::True, "false" => Token::False, "let" => Token::Let, @@ -903,7 +904,9 @@ impl<'a> TokenIterator<'a> { #[cfg(not(feature = "no_function"))] "fn" => Token::Fn, - _ => Token::Identifier(out), + _ if has_letter => Token::Identifier(identifier), + + _ => Token::LexError(LERR::MalformedIdentifier(identifier)), }, pos, )); @@ -1904,14 +1907,17 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result s, - Some((_, pos)) => return Err(ParseError::new(PERR::VarExpectsIdentifier, pos)), - None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())), + Some((Token::LexError(s), pos)) => { + return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) + } + Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)), + None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())), }; match input.next() { - Some((Token::In, _)) => {} - Some((_, pos)) => return Err(ParseError::new(PERR::VarExpectsIdentifier, pos)), - None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())), + Some((Token::In, _)) => (), + Some((_, pos)) => return Err(ParseError::new(PERR::MissingIn, pos)), + None => return Err(ParseError::new(PERR::MissingIn, Position::eof())), } let expr = parse_expr(input)?; @@ -1932,8 +1938,11 @@ fn parse_var<'a>( let name = match input.next() { Some((Token::Identifier(s), _)) => s, - Some((_, pos)) => return Err(ParseError::new(PERR::VarExpectsIdentifier, pos)), - None => return Err(ParseError::new(PERR::VarExpectsIdentifier, Position::eof())), + Some((Token::LexError(s), pos)) => { + return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) + } + Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)), + None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())), }; if matches!(input.peek(), Some(&(Token::Equals, _))) { From f36caa6dc32fc7ad1881aebd7cebef01b0c55123 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 12:40:42 +0800 Subject: [PATCH 16/21] Add optimize_full pseudo feature. --- Cargo.toml | 3 ++- src/engine.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7eb97fb9..308685bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ num-traits = "*" [features] #default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] -default = [] +default = [ "optimize_full" ] debug_msgs = [] # print debug messages on function registrations and calls unchecked = [] # unchecked arithmetic no_stdlib = [] # no standard library of utility functions @@ -27,6 +27,7 @@ no_index = [] # no arrays and indexing no_float = [] # no floating-point no_function = [] # no script-defined functions no_optimize = [] # no script optimizer +optimize_full = [] # set optimization level to Full (default is Simple) only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types diff --git a/src/engine.rs b/src/engine.rs index 6b951d4e..bee73cff 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -99,14 +99,20 @@ impl Engine<'_> { // Create the new scripting Engine let mut engine = Engine { - #[cfg(not(feature = "no_optimize"))] - optimization_level: OptimizationLevel::Full, ext_functions: HashMap::new(), script_functions: Vec::new(), type_iterators: HashMap::new(), type_names, on_print: Box::new(default_print), // default print/debug implementations on_debug: Box::new(default_print), + + #[cfg(not(feature = "no_optimize"))] + #[cfg(not(feature = "optimize_full"))] + optimization_level: OptimizationLevel::Simple, + + #[cfg(not(feature = "no_optimize"))] + #[cfg(feature = "optimize_full")] + optimization_level: OptimizationLevel::Full, }; engine.register_core_lib(); From 6d33a91d09e497040d1069fa5a4a43f10ab39015 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 12:40:55 +0800 Subject: [PATCH 17/21] Fix minor floating-point digit error. --- tests/power_of.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/power_of.rs b/tests/power_of.rs index f02a2e52..fb309088 100644 --- a/tests/power_of.rs +++ b/tests/power_of.rs @@ -14,7 +14,7 @@ fn test_power_of() -> Result<(), EvalAltResult> { { assert_eq!( engine.eval::("2.2 ~ 3.3")?, - 13.489468760533386 as FLOAT + 13.489468760533388 as FLOAT ); assert_eq!(engine.eval::("2.0~-2.0")?, 0.25 as FLOAT); assert_eq!(engine.eval::("(-2.0~-2.0)")?, 0.25 as FLOAT); @@ -36,7 +36,7 @@ fn test_power_of_equals() -> Result<(), EvalAltResult> { { assert_eq!( engine.eval::("let x = 2.2; x ~= 3.3; x")?, - 13.489468760533386 as FLOAT + 13.489468760533388 as FLOAT ); assert_eq!( engine.eval::("let x = 2.0; x ~= -2.0; x")?, From 5235ba71dd36756a44b0607eb54186b60c836afe Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 12:41:19 +0800 Subject: [PATCH 18/21] Run examples with full optimizations. --- examples/repl.rs | 3 ++- examples/rhai_runner.rs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 76bdcd48..1187d1c9 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -1,6 +1,7 @@ +use rhai::{Engine, EvalAltResult, Scope, AST}; + #[cfg(not(feature = "no_optimize"))] use rhai::OptimizationLevel; -use rhai::{Engine, EvalAltResult, Scope, AST}; use std::{ io::{stdin, stdout, Write}, diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 59fe6770..4da91feb 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -1,6 +1,8 @@ +use rhai::{Engine, EvalAltResult}; + #[cfg(not(feature = "no_optimize"))] use rhai::OptimizationLevel; -use rhai::{Engine, EvalAltResult}; + use std::{env, fs::File, io::Read, iter, process::exit}; fn padding(pad: &str, len: usize) -> String { From 08abf07f835954dcd5e19598d2ba249423b39852 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 12:41:52 +0800 Subject: [PATCH 19/21] Add string literal indexing and functon default value to optimizer. --- src/optimize.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/optimize.rs b/src/optimize.rs index cadadd59..8a75a905 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -270,11 +270,19 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { (Expr::Array(mut items, _), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < items.len() && items.iter().all(|x| x.is_pure()) => { - // Array where everything is a pure - promote the indexed item. + // Array literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); items.remove(i as usize) } + (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) + if i >= 0 && (i as usize) < s.chars().count() => + { + // String literal indexing - get the character + state.set_dirty(); + Expr::CharConstant(s.chars().nth(i as usize).expect("should get char"), pos) + } + (lhs, rhs) => Expr::Index( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), @@ -350,16 +358,13 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { let engine = state.engine.expect("engine should be Some"); let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect(); - if let Ok(r) = engine.call_ext_fn_raw(&id, call_args, pos) { - r.and_then(|result| map_dynamic_to_expr(result, pos).0) + engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| + r.or(def_value.clone()).and_then(|result| map_dynamic_to_expr(result, pos).0) .map(|expr| { state.set_dirty(); expr - }) + })).flatten() .unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) - } else { - Expr::FunctionCall(id, args, def_value, pos) - } } // Optimize the function call arguments Expr::FunctionCall(id, args, def_value, pos) => { From 35e354eb4130f88523bad79a8d08a9cfe295313f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 12:42:01 +0800 Subject: [PATCH 20/21] Refine README. --- README.md | 443 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 298 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 1a346f23..f93b6eba 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,26 @@ Rhai - Embedded Scripting for Rust ================================= -Rhai is an embedded scripting language for Rust that gives you a safe and easy way to add scripting to your applications. +Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application. Rhai's current feature set: -* Easy integration with Rust functions and data types -* Fairly efficient (1 mil iterations in 0.75 sec on my 5 year old laptop) +* Easy integration with Rust functions and data types, supporting getter/setter methods +* Easily call a script-defined function from Rust +* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop) * Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app) * Easy-to-use language similar to JS+Rust * Support for overloaded functions -* Very few additional dependencies (right now only [`num-traits`] to do checked arithmetic operations) +* Compiled script is optimized for repeat evaluations +* Very few additional dependencies (right now only [`num-traits`] to do checked arithmetic operations); + For [`no_std`] builds, a number of additional dependencies are pulled in to provide for basic library functionalities. **Note:** Currently, the version is 0.10.2, so the language and API's may change before they stabilize. Installation ------------ -You can install Rhai using crates by adding this line to your dependencies: +Install the Rhai crate by adding this line to `dependencies`: ```toml [dependencies] @@ -33,7 +36,7 @@ rhai = "*" to use the latest version. -Beware that in order to use pre-releases (alpha and beta) you need to specify the exact version in your `Cargo.toml`. +Beware that in order to use pre-releases (e.g. alpha and beta), the exact version must be specified in the `Cargo.toml`. Optional features ----------------- @@ -43,14 +46,14 @@ Optional features | `debug_msgs` | Print debug messages to stdout related to function registrations and calls. | | `no_stdlib` | Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. Standard types are not affected. | | `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! | -| `no_function` | Disable script-defined functions if you don't need them. | -| `no_index` | Disable arrays and indexing features if you don't need them. | -| `no_float` | Disable floating-point numbers and math if you don't need them. | +| `no_function` | Disable script-defined functions if not needed. | +| `no_index` | Disable arrays and indexing features if not needed. | +| `no_float` | Disable floating-point numbers and math if not needed. | | `no_optimize` | Disable the script optimizer. | | `only_i32` | Set the system integer type to `i32` and disable all other integer types. | | `only_i64` | Set the system integer type to `i64` and disable all other integer types. | -By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here for you to opt-**out** of certain functionalities that you do not need. +By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed. Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language. Related @@ -59,7 +62,7 @@ Related Other cool projects to check out: * [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) +* 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 -------- @@ -107,10 +110,10 @@ There are also a number of examples scripts that showcase Rhai's features, all i | `string.rhai` | string operations | | `while.rhai` | while loop | -| Example scripts | Description | -| ----------------- | ----------------------------------------------------------------- | -| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter | -| `primes.rhai` | use Sieve of Eratosthenes to find all primes smaller than a limit | +| Example scripts | Description | +| ----------------- | ---------------------------------------------------------------------------------- | +| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | +| `primes.rhai` | use Sieve of Eratosthenes to find all primes smaller than a limit | To run the scripts, either make a tiny program or use of the `rhai_runner` example: @@ -138,13 +141,24 @@ fn main() -> Result<(), EvalAltResult> } ``` -You can also evaluate a script file: +The type parameter is used to specify the type of the return value, which _must_ match the actual type or an error is returned. +Rhai is very strict here. There are two ways to specify the return type - _turbofish_ notation, or type inference. ```rust -let result = engine.eval_file::("hello_world.rhai")?; +let result = engine.eval::("40 + 2")?; // return type is i64, specified using 'turbofish' notation + +let result: i64 = engine.eval("40 + 2")?; // return type is inferred to be i64 + +let result = engine.eval("40 + 2")?; // returns an error because the actual return type is i64, not String ``` -If you want to repeatedly evaluate a script, you can _compile_ it first into an AST (abstract syntax tree) form: +Evaluate a script file directly: + +```rust +let result = engine.eval_file::("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf' +``` + +To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form: ```rust use rhai::Engine; @@ -155,7 +169,7 @@ let mut engine = Engine::new(); let ast = engine.compile("40 + 2")?; for _ in 0..42 { - let result = engine.eval_ast::(&ast)?; + let result: i64 = engine.eval_ast(&ast)?; println!("Answer: {}", result); // prints 42 } @@ -168,11 +182,10 @@ use rhai::Engine; let mut engine = Engine::new(); -let ast = engine.compile_file("hello_world.rhai".into()).unwrap(); +let ast = engine.compile_file("hello_world.rhai".into())?; ``` -Rhai also allows you to work _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust. -You do this via `call_fn`: +Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust - via `call_fn`: ```rust use rhai::Engine; @@ -180,21 +193,20 @@ use rhai::Engine; let mut engine = Engine::new(); // Define a function in a script and load it into the Engine. -engine.consume( - r" +engine.consume(true, // pass true to 'retain_functions' otherwise these functions + r" // will be cleared at the end of consume() fn hello(x, y) { // a function with two parameters: String and i64 - x.len() + y // returning i64 + x.len() + y // returning i64 } fn hello(x) { // functions can be overloaded: this one takes only one parameter - x * 2 // returning i64 + x * 2 // returning i64 } - ", true)?; // pass true to 'retain_functions' otherwise these functions - // will be cleared at the end of consume() + ")?; // Evaluate the function in the AST, passing arguments into the script as a tuple // if there are more than one. Beware, arguments must be of the correct types because -// Rhai does not have built-in type conversions. If you pass in arguments of the wrong type, +// Rhai does not have built-in type conversions. If arguments of the wrong types are passed, // the Engine will not find the function. let result: i64 = engine.call_fn("hello", &ast, ( String::from("abc"), 123_i64 ) )?; @@ -219,19 +231,20 @@ The following primitive types are supported natively: | **Dynamic** (i.e. can be anything) | `rhai::Dynamic` | | **System** (current configuration) | `rhai::INT` (`i32` or `i64`),
`rhai::FLOAT` (`f32` or `f64`) | -All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different; you cannot even add them together. +All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust. -The default integer type is `i64`. If you do not need any other integer type, you can enable the [`only_i64`] feature. +The default integer type is `i64`. If other integer types are not needed, it is possible to exclude them and make a smaller build with the [`only_i64`] feature. -If you only need 32-bit integers, you can enable the [`only_i32`] feature and remove support for all integer types other than `i32` including `i64`. -This is useful on 32-bit systems where using 64-bit integers incurs a performance penalty. +If only 32-bit integers are needed, enabling the [`only_i32`] feature will remove support for all integer types other than `i32`, including `i64`. +This is useful on 32-bit systems where using 64-bit integers incur a performance penalty. -If you do not need floating-point, enable the [`no_float`] feature to remove support. +If no floating-point is needed, use the [`no_float`] feature to remove support. Value conversions ----------------- -There is a `to_float` function to convert a supported number to an `f64`, and a `to_int` function to convert a supported number to `i64` and that's about it. For other conversions you can register your own conversion functions. +There is a `to_float` function to convert a supported number to an `f64`, and a `to_int` function to convert a supported number to `i64` and that's about it. +For other conversions, register custom conversion functions. There is also a `type_of` function to detect the type of a value. @@ -257,7 +270,8 @@ if z.type_of() == "string" { Working with functions ---------------------- -Rhai's scripting engine is very lightweight. It gets its ability from the functions in your program. To call these functions, you need to register them with the scripting engine. +Rhai's scripting engine is very lightweight. It gets most of its abilities from functions. +To call these functions, they need to be registered with the engine. ```rust use rhai::{Engine, EvalAltResult}; @@ -310,7 +324,7 @@ fn decide(yes_no: bool) -> Dynamic { Generic functions ----------------- -Generic functions can be used in Rhai, but you'll need to register separate instances for each concrete type: +Generic functions can be used in Rhai, but separate instances for each concrete type must be registered separately: ```rust use std::fmt::Display; @@ -331,14 +345,15 @@ fn main() } ``` -You can also see in this example how you can register multiple functions (or in this case multiple instances of the same function) to the same name in script. This gives you a way to overload functions the correct one, based on the types of the parameters, from your script. +This example shows how to register multiple functions (or, in this case, multiple instances of the same function) to the same name in script. +This enables function overloading based on the number and types of parameters. Fallible functions ------------------ -If your function is _fallible_ (i.e. it returns a `Result<_, Error>`), you can register it with `register_result_fn` (using the `RegisterResultFn` trait). +If a function is _fallible_ (i.e. it returns a `Result<_, Error>`), it can be registered with `register_result_fn` (using the `RegisterResultFn` trait). -Your function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements `From<&str>` and `From` etc. and the error text gets converted into `EvalAltResult::ErrorRuntime`. +The function must return `Result<_, EvalAltResult>`. `EvalAltResult` implements `From<&str>` and `From` etc. and the error text gets converted into `EvalAltResult::ErrorRuntime`. ```rust use rhai::{Engine, EvalAltResult, Position}; @@ -487,12 +502,12 @@ let x = new_ts(); print(x.type_of()); // prints "foo::bar::TestStruct" ``` -If you use `register_type_with_name` to register the custom type with a special pretty-print name, `type_of` will return that instead. +If `register_type_with_name` is used to register the custom type with a special "pretty-print" name, `type_of` will return that name instead. Getters and setters ------------------- -Similarly, you can work with members of your custom types. This works by registering a 'get' or a 'set' function for working with your struct. +Similarly, custom types can expose members by registering a `get` and/or `set` function. For example: @@ -531,9 +546,11 @@ println!("result: {}", result); Initializing and maintaining state --------------------------------- -By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions that have been defined but no top-level state. This gives each one a fairly clean starting place. Sometimes, though, you want to continue using the same top-level state from one invocation to the next. +By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions that have been defined but no global state. +This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next, +such a state must be manually created and passed in. -In this example, we first create a state with a few initialized variables, then thread the same state through multiple invocations: +In this example, a global state object (a `Scope`) is created with a few initialized variables, then the same state is threaded through multiple invocations: ```rust use rhai::{Engine, Scope, EvalAltResult}; @@ -569,22 +586,25 @@ fn main() -> Result<(), EvalAltResult> } ``` -Rhai Language guide +Rhai Language Guide =================== Comments -------- +Comments are C-style, including '`/*` ... `*/`' pairs and '`//`' for comments to the end of the line. + ```rust let /* intruder comment */ name = "Bob"; + // This is a very important comment + /* This comment spans multiple lines, so it only makes sense that it is even more important */ -/* Fear not, Rhai satisfies all your nesting - needs with nested comments: +/* Fear not, Rhai satisfies all nesting needs with nested comments: /*/*/*/*/**/*/*/*/*/ */ ``` @@ -592,46 +612,85 @@ let /* intruder comment */ name = "Bob"; Variables --------- -Variables in Rhai follow normal naming rules (i.e. must contain only ASCII letters, digits and '`_`' underscores). +Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). + +Variable names must start with an ASCII letter or an underscore '`_`', and must contain at least one ASCII letter within. +Therefore, names like '`_`', '`_42`' etc. are not legal variable names. Variable names are also case _sensitive_. + +Variables are defined using the `let` keyword. ```rust -let x = 3; +let x = 3; // ok +let _x = 42; // ok +let x_ = 42; // also ok +let _x_ = 42; // still ok + +let _ = 123; // syntax error - illegal variable name +let _9 = 9; // syntax error - illegal variable name + +let x = 42; // variable is 'x', lower case +let X = 123; // variable is 'X', upper case +x == 42; +X == 123; ``` Constants --------- -Constants can be defined and are immutable. Constants follow the same naming rules as [variables](#variables). +Constants can be defined using the `const` keyword 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 +x = 123; // syntax error - cannot assign to constant ``` Constants must be assigned a _value_ not an expression. ```rust -const x = 40 + 2; // <- syntax error - cannot assign expression to constant +const x = 40 + 2; // syntax error - cannot assign expression to constant ``` - Numbers ------- -| Format | Type | -| ---------------- | ---------------------------------------------- | -| `123_345`, `-42` | `i64` in decimal, '`_`' separators are ignored | -| `0o07_76` | `i64` in octal, '`_`' separators are ignored | -| `0xabcd_ef` | `i64` in hex, '`_`' separators are ignored | -| `0b0101_1001` | `i64` in binary, '`_`' separators are ignored | -| `123_456.789` | `f64`, '`_`' separators are ignored | +Integer numbers follow C-style format with support for decimal, binary ('`0b`'), octal ('`0o`') and hex ('`0x`') notations. + +The default system integer type (also aliased to `INT`) is `i64`. It can be turned into `i32` via the [`only_i32`] feature. + +Floating-point numbers are also supported if not disabled with [`no_float`]. The default system floating-point type is `i64` (also aliased to `FLOAT`). + +'`_`' separators can be added freely and are ignored within a number. + +| Format | Type | +| ---------------- | ---------------- | +| `123_345`, `-42` | `i64` in decimal | +| `0o07_76` | `i64` in octal | +| `0xabcd_ef` | `i64` in hex | +| `0b0101_1001` | `i64` in binary | +| `123_456.789` | `f64` | Numeric operators ----------------- +Numeric operators generally follow C styles. + +| Operator | Description | Integers only | +| -------- | ----------------------------------------------------------- | :-----------: | +| `+` | Plus | | +| `-` | Minus | | +| `*` | Multiply | | +| `/` | Divide (C-style integer division if acted on integer types) | | +| `%` | Modulo (remainder) | | +| `~` | Power | | +| `&` | Binary _And_ bit-mask | Yes | +| `|` | Binary _Or_ bit-mask | Yes | +| `^` | Binary _Xor_ bit-mask | Yes | +| `<<` | Left bit-shift | Yes | +| `>>` | Right bit-shift | Yes | + ```rust -let x = (1 + 2) * (6 - 4) / 2; // arithmetic +let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses let reminder = 42 % 10; // modulo let power = 42 ~ 2; // power (i64 and f64 only) let left_shifted = 42 << 3; // left shift @@ -642,10 +701,14 @@ let bit_op = 42 | 99; // bit masking Unary operators --------------- +| Operator | Description | +| -------- | ----------- | +| `+` | Plus | +| `-` | Negative | + ```rust let number = -5; number = -5 - +5; -let boolean = !true; ``` Numeric functions @@ -677,6 +740,17 @@ The following standard functions (defined in the standard library but excluded i Strings and Chars ----------------- +String and char literals follow C-style formatting, with support for Unicode ('`\u`') and hex ('`\x`') escape sequences. + +Although internally Rhai strings are stored as UTF-8 just like in Rust (they _are_ Rust `String`s), +in the Rhai language they can be considered a stream of Unicode characters, and can be directly indexed (unlike Rust). +Individual characters within a Rhai string can be replaced. In Rhai, there is no separate concepts of `String` and `&str` as in Rust. + +Strings can be built up from other strings and types via the `+` operator (provided by the standard library but excluded if [`no_stdlib`]). +This is particularly useful when printing output. + +`type_of()` a string returns `"string"`. + ```rust let name = "Bob"; let middle_initial = 'C'; @@ -690,27 +764,28 @@ let age = 42; let record = full_name + ": age " + age; record == "Bob C. Davis: age 42"; -// Strings can be indexed to get a character -// (disabled with the 'no_index' feature) +// Unlike Rust, Rhai strings can be indexed to get a character +// (disabled with 'no_index') let c = record[4]; c == 'C'; -ts.s = record; +ts.s = record; // custom type properties can take strings let c = ts.s[4]; c == 'C'; -let c = "foo"[0]; +let c = "foo"[0]; // indexing also works on string literals... c == 'f'; -let c = ("foo" + "bar")[5]; +let c = ("foo" + "bar")[5]; // ... and expressions returning strings c == 'r'; // Escape sequences in strings record += " \u2764\n"; // escape sequence of '❤' in Unicode record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line -// Unlike Rust, Rhai strings can be modified +// Unlike Rust, Rhai strings can be directly modified character-by-character +// (disabled with 'no_index') record[4] = '\x58'; // 0x58 = 'X' record == "Bob X. Davis: age 42 ❤\n"; ``` @@ -760,7 +835,12 @@ full_name.len() == 0; Arrays ------ -You can create arrays of values, and then access them with numeric indices. +Arrays are first-class citizens in Rhai. Like C, arrays are accessed with zero-based, non-negative integer indices. +Array literals are built within square brackets '`[`' ,, '`]`' and separated by commas '`,`'. + +The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`. + +Arrays are disabled via the [`no_index`] feature. The following functions (defined in the standard library but excluded if [`no_stdlib`]) operate on arrays: @@ -777,7 +857,7 @@ The following functions (defined in the standard library but excluded if [`no_st Examples: ```rust -let y = [1, 2, 3]; // 3 elements +let y = [1, 2, 3]; // array literal with 3 elements y[1] = 42; print(y[1]); // prints 42 @@ -789,7 +869,9 @@ foo == 42; let foo = [1, 2, 3][0]; foo == 1; -fn abc() { [42, 43, 44] } +fn abc() { + [42, 43, 44] // a function returning an array literal +} let foo = abc()[0]; foo == 42; @@ -823,8 +905,7 @@ y.clear(); // empty the array print(y.len()); // prints 0 ``` -`push` and `pad` are only defined for standard built-in types. If you want to use them with -your own custom type, you need to register a type-specific version: +`push` and `pad` are only defined for standard built-in types. For custom types, type-specific versions must be registered: ```rust engine.register_fn("push", @@ -832,27 +913,45 @@ engine.register_fn("push", ); ``` -The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`. - -Arrays are disabled via the [`no_index`] feature. - Comparison operators -------------------- -You can compare most values of the same data type. If you compare two values of _different_ data types, the result is always `false`. +Comparing most values of the same data type work out-of-the-box for standard types supported by the system. + +However, if the [`no_stdlib`] feature is turned on, comparisons can only be made between restricted system +types - `INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), string, array, `bool`, `char`. ```rust 42 == 42; // true 42 > 42; // false "hello" > "foo"; // true "42" == 42; // false +``` + +Comparing two values of _different_ data types, or of unknown data types, always results in `false`. + +```rust 42 == 42.0; // false - i64 is different from f64 +42 > "42"; // false - i64 is different from string +42 <= "42"; // false again + +let ts = new_ts(); // custom type +ts == 42; // false - types are not the same ``` Boolean operators ----------------- -Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. +| Operator | Description | +| -------- | ------------------------------- | +| `!` | Boolean _Not_ | +| `&&` | Boolean _And_ (short-circuits) | +| `||` | Boolean _Or_ (short-circuits) | +| `&` | Boolean _And_ (full evaluation) | +| `|` | Boolean _Or_ (full evaluation) | + +Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated +if the first one already proves the condition wrong. Single boolean operators `&` and `|` always evaluate both operands. @@ -888,34 +987,45 @@ my_str += 12345; my_str == "abcABC12345" ``` -If --- +If statements +------------- + +All branches of an `if` statement must be enclosed within braces '`{`' .. '`}`', even when there is only one statement. + +Like Rust, there is no ambiguity regarding which `if` clause a statement belongs to. ```rust if true { print("It's true!"); } else if true { print("It's true again!"); +} else if ... { + : +} else if ... { + : } else { - print("It's false!"); + print("It's finally false!"); } + +if (decision) print("I've decided!"); +// ^ syntax error, expecting '{' in statement block ``` -While ------ +While loops +----------- ```rust let x = 10; while x > 0 { print(x); - if x == 5 { break; } + if x == 5 { break; } // break out of while loop x = x - 1; } ``` -Loop ----- +Infinite loops +-------------- ```rust let x = 10; @@ -923,12 +1033,14 @@ let x = 10; loop { print(x); x = x - 1; - if x == 0 { break; } + if x == 0 { break; } // break out of loop } ``` -For ---- +For loops +--------- + +Iterating through a range or an array is provided by the `for` ... `in` loop. ```rust let array = [1, 3, 5, 7, 9, 42]; @@ -946,32 +1058,34 @@ for x in range(0, 50) { } ``` -Return ------- +Returning values +---------------- ```rust -return; // equivalent to return (); +return; // equivalent to return (); -return 123 + 456; +return 123 + 456; // returns 579 ``` -Errors and Exceptions +Errors and exceptions --------------------- +All of `Engine`'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. +To deliberately return an error during an evaluation, use the `throw` keyword. + ```rust if some_bad_condition_has_happened { throw error; // 'throw' takes a string to form the exception text } -throw; // no exception text +throw; // empty exception text: "" ``` -All of `Engine`'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. - -Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(reason, position))` with the exception text captured by the `reason` parameter. +Exceptions thrown via `throw` in the script can be captured by matching `Err(EvalAltResult::ErrorRuntime(`_reason_`, `_position_`))` +with the exception text captured by the first parameter. ```rust -let result = engine.eval::(&mut scope, r#" +let result = engine.eval::(r#" let x = 42; if x > 0 { @@ -995,34 +1109,44 @@ fn add(x, y) { print(add(2, 3)); ``` -Just like in Rust, you can also use an implicit return. +Just like in Rust, an implicit return can be used. In fact, the last statement of a block is _always_ the block's return value +regardless of whether it is terminated with a semicolon `;`. This is different from Rust. ```rust fn add(x, y) { - x + y + x + y; // value of the last statement is used as the function's return value } -print(add(2, 3)); +fn add2(x) { + return x + 2; // explicit return +} + +print(add(2, 3)); // prints 5 +print(add2(42)); // prints 44 ``` +### Passing arguments by value + Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). -It is important to remember that all parameters are passed by _value_, so all functions are _pure_ (i.e. they never modify their parameters). -Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if you are not careful. +It is important to remember that all arguments are passed by _value_, so all functions are _pure_ (i.e. they never modify their arguments). +Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if not careful. ```rust -fn change(s) { - s = 42; // only a COPY of 'x' is changed +fn change(s) { // 's' is passed by value + s = 42; // only a COPY of 's' is changed } let x = 500; -x.change(); +x.change(); // desugars to change(x) x == 500; // 'x' is NOT changed! ``` -Functions can only be defined at the top level, never inside a block or another function. +### Global definitions only + +Functions can only be defined at the global level, never inside a block or another function. ```rust -// Top level is OK +// Global level is OK fn add(x, y) { x + y } @@ -1037,7 +1161,9 @@ fn do_addition(x) { } ``` -Functions can be _overloaded_ based on the number of parameters (but not parameter types, since all parameters are [`Dynamic`]). +### Functions overloading + +Functions can be _overloaded_ based on the _number_ of parameters (but not parameter _types_, since all parameters are the same type - [`Dynamic`]). New definitions of the same name and number of parameters overwrite previous definitions. ```rust @@ -1056,15 +1182,19 @@ abc(); // prints "None." Members and methods ------------------- +Properties and methods in a Rust custom type registered with the engine can be called just like in Rust: + ```rust -let a = new_ts(); -a.x = 500; -a.update(); +let a = new_ts(); // constructor function +a.x = 500; // property access +a.update(); // method call ``` `print` and `debug` ------------------- +The `print` and `debug` functions default to printing to `stdout`, with `debug` using standard debug formatting. + ```rust print("hello"); // prints hello to stdout print(1 + 2 + 3); // prints 6 to stdout @@ -1074,6 +1204,9 @@ debug("world!"); // prints "world!" to stdout using debug formatting ### Overriding `print` and `debug` with callback functions +When embedding Rhai into an application, it is usually necessary to trap `print` and `debug` output +(for logging into a tracking log, for example). + ```rust // Any function or closure that takes an &str argument can be used to override // print and debug @@ -1134,15 +1267,18 @@ Constants propagation is used to remove dead code: 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 -if true { print("done!"); } // <-- the line above is equivalent to this -print("done!"); // <-- the line above is further simplified to this +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 ``` -These are quite effective for template-based machine-generated scripts where certain constant values are spliced into the script text in order to turn on/off certain sections. -For fixed script texts, the constant values can be provided in a user-defined `Scope` object to the `Engine` for use in compilation and evaluation. +These are quite effective for template-based machine-generated scripts where certain constant values +are spliced into the script text in order to turn on/off certain sections. +For fixed script texts, the constant values can be provided in a user-defined `Scope` object +to the `Engine` for use in compilation and evaluation. -Beware, however, that most operators are actually function calls, and those functions can be overridden, so they are not optimized away: +Beware, however, that most operators are actually function calls, and those functions can be overridden, +so they are not optimized away: ```rust const DECISION = 1; @@ -1159,7 +1295,8 @@ if DECISION == 1 { // NOT optimized away because you can define } ``` -So, instead, do this: +because no operator functions will be run (in order not to trigger side effects) during the optimization process +(unless the optimization level is set to [`OptimizationLevel::Full`]). So, instead, do this: ```rust const DECISION_1 = true; @@ -1177,7 +1314,8 @@ if DECISION_1 { } ``` -In general, boolean constants are most effective if you want the optimizer to automatically prune large `if`-`else` branches because they do not depend on operators. +In general, boolean constants are most effective for the optimizer to automatically prune +large `if`-`else` branches because they do not depend on operators. Alternatively, turn the optimizer to [`OptimizationLevel::Full`] @@ -1188,21 +1326,30 @@ Here be dragons! There are actually three levels of optimizations: `None`, `Simple` and `Full`. -`None` is obvious - no optimization on the AST is performed. +* `None` is obvious - no optimization on the AST is performed. -`Simple` performs relatively _safe_ optimizations without causing side effects (i.e. it only relies on static analysis and will not actually perform any function calls). `Simple` is the default. +* `Simple` (default) performs relatively _safe_ optimizations without causing side effects + (i.e. it only relies on static analysis and will not actually perform any function calls). -`Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators. +* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. + One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators. + +An engine's optimization level is set via a call to `set_optimization_level`: ```rust -// The following run with OptimizationLevel::Full +// Turn on aggressive optimizations +engine.set_optimization_level(rhai::OptimizationLevel::Full); +``` + +```rust +// When compiling the following with OptimizationLevel::Full... const DECISION = 1; - -if DECISION == 1 { // this condition is now eliminated because 'DECISION == 1' is a function call to the '==' function, and it returns 'true' - print("hello!"); // the 'true' block is promoted to the parent level + // this condition is now eliminated because 'DECISION == 1' +if DECISION == 1 { // is a function call to the '==' function, and it returns 'true' + print("hello!"); // this block is promoted to the parent level } else { - print("boo!"); // the 'else' block is eliminated + print("boo!"); // this block is eliminated because it is never reached } print("hello!"); // <- the above is equivalent to this @@ -1211,32 +1358,35 @@ print("hello!"); // <- the above is equivalent to this ### Side effect considerations All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe. -Beware, however, that if custom functions are registered, they'll also be called. If custom functions are registered to replace built-in operator functions, -the custom functions will be called and _may_ cause side-effects. +Beware, however, that if custom functions are registered, they'll also be called. +If custom functions are registered to replace built-in operator functions, the custom functions will be called +and _may_ cause side-effects. + +Therefore, when using [`OptimizationLevel::Full`], it is recommended that registrations of custom functions be held off +until _after_ the compilation process. ### Subtle semantic changes -Some optimizations can be quite aggressive and can alter subtle semantics of the script. For example: +Some optimizations can alter subtle semantics of the script. For example: ```rust -if true { // <-- condition always true - 123.456; // <-- eliminated - hello; // <-- eliminated, EVEN THOUGH the variable doesn't exist! - foo(42) // <-- promoted up-level +if true { // condition always true + 123.456; // eliminated + hello; // eliminated, EVEN THOUGH the variable doesn't exist! + foo(42) // promoted up-level } -// The above optimizes to: - -foo(42) +foo(42) // <- the above optimizes to this ``` -Nevertheless, if you would be evaluating the original script, it would have been an error - the variable `hello` doesn't exist, so the script would have been terminated at that point with an error return. +Nevertheless, if the original script were evaluated instead, it would have been an error - the variable `hello` doesn't exist, +so the script would have been terminated at that point with an error return. -In fact, any errors inside a statement that has been eliminated will silently _go away_: +In fact, any errors inside a statement that has been eliminated will silently _disappear_: ```rust print("start!"); -if my_decision { /* do nothing... */ } // <-- eliminated due to no effect +if my_decision { /* do nothing... */ } // eliminated due to no effect print("end!"); // The above optimizes to: @@ -1246,16 +1396,19 @@ print("end!"); ``` In the script above, if `my_decision` holds anything other than a boolean value, the script should have been terminated due to a type error. -However, after optimization, the entire `if` statement is removed, thus the script silently runs to completion without errors. +However, after optimization, the entire `if` statement is removed (because an access to `my_decision` produces no side effects), +thus the script silently runs to completion without errors. ### Turning off optimizations It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess), -there is a setting in `Engine` to turn off optimizations. +turn it off by setting the optimization level to `OptimizationLevel::None`. ```rust let engine = rhai::Engine::new(); -engine.set_optimization_level(rhai::OptimizationLevel::None); // turn off the optimizer + +// Turn off the optimizer +engine.set_optimization_level(rhai::OptimizationLevel::None); ``` From f3213b945dd20c5b4fdd36368194f93e38719ead Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 13:08:53 +0800 Subject: [PATCH 21/21] Fix power_of test. --- tests/power_of.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/power_of.rs b/tests/power_of.rs index fb309088..c82e4d93 100644 --- a/tests/power_of.rs +++ b/tests/power_of.rs @@ -3,6 +3,9 @@ use rhai::{Engine, EvalAltResult, INT}; #[cfg(not(feature = "no_float"))] use rhai::FLOAT; +#[cfg(not(feature = "no_float"))] +const EPSILON: FLOAT = 0.0000000001; + #[test] fn test_power_of() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); @@ -12,9 +15,8 @@ fn test_power_of() -> Result<(), EvalAltResult> { #[cfg(not(feature = "no_float"))] { - assert_eq!( - engine.eval::("2.2 ~ 3.3")?, - 13.489468760533388 as FLOAT + assert!( + (engine.eval::("2.2 ~ 3.3")? - 13.489468760533386 as FLOAT).abs() <= EPSILON ); assert_eq!(engine.eval::("2.0~-2.0")?, 0.25 as FLOAT); assert_eq!(engine.eval::("(-2.0~-2.0)")?, 0.25 as FLOAT); @@ -34,9 +36,9 @@ fn test_power_of_equals() -> Result<(), EvalAltResult> { #[cfg(not(feature = "no_float"))] { - assert_eq!( - engine.eval::("let x = 2.2; x ~= 3.3; x")?, - 13.489468760533388 as FLOAT + assert!( + (engine.eval::("let x = 2.2; x ~= 3.3; x")? - 13.489468760533386 as FLOAT).abs() + <= EPSILON ); assert_eq!( engine.eval::("let x = 2.0; x ~= -2.0; x")?,