From 32672b184bceb85daa741fab2db2fe4a532ca47a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Apr 2020 12:29:01 +0800 Subject: [PATCH 01/21] Avoid copying strings. --- src/engine.rs | 8 ++-- src/optimize.rs | 2 +- src/parser.rs | 104 +++++++++++++++++++++++++++--------------------- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 8f4f418f..cd4f4b2c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1191,7 +1191,7 @@ impl Engine<'_> { Expr::FloatConstant(f, _) => Ok(f.into_dynamic()), Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()), - Expr::StringConstant(s, _) => Ok(s.into_dynamic()), + Expr::StringConstant(s, _) => Ok(s.clone().into_owned().into_dynamic()), Expr::CharConstant(c, _) => Ok(c.into_dynamic()), Expr::Variable(id, pos) => Self::search_scope(scope, id, *pos).map(|(_, val)| val), Expr::Property(_, _) => panic!("unexpected property."), @@ -1213,7 +1213,9 @@ impl Engine<'_> { // name = rhs Expr::Variable(name, pos) => match scope .get(name) - .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(name.clone(), *pos))? + .ok_or_else(|| { + EvalAltResult::ErrorVariableNotFound(name.clone().into_owned(), *pos) + })? .0 { entry @@ -1328,7 +1330,7 @@ impl Engine<'_> { && engine.fn_lib.as_ref().unwrap().has_function(name, 1)) } - match fn_name.as_str() { + match fn_name.as_ref() { // Dump AST KEYWORD_DUMP_AST => { let pos = if args_expr_list.is_empty() { diff --git a/src/optimize.rs b/src/optimize.rs index ec5d3688..22693977 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -444,7 +444,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::FunctionCall(id, args, def_value, pos), // Do not call some special keywords - Expr::FunctionCall(id, args, def_value, pos) if DONT_EVAL_KEYWORDS.contains(&id.as_str())=> + Expr::FunctionCall(id, args, def_value, pos) if DONT_EVAL_KEYWORDS.contains(&id.as_ref())=> Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), // Eagerly call functions diff --git a/src/parser.rs b/src/parser.rs index 8dcbbc98..b592af43 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -13,7 +13,9 @@ use crate::stdlib::{ boxed::Box, char, collections::HashMap, - fmt, format, + fmt, + fmt::Display, + format, iter::Peekable, ops::Add, rc::Rc, @@ -316,11 +318,11 @@ pub enum Stmt { /// loop { stmt } Loop(Box), /// for id in expr { stmt } - For(String, Box, Box), + For(Cow<'static, str>, Box, Box), /// let id = expr - Let(String, Option>, Position), + Let(Cow<'static, str>, Option>, Position), /// const id = expr - Const(String, Box, Position), + Const(Cow<'static, str>, Box, Position), /// { stmt; ... } Block(Vec, Position), /// { stmt } @@ -401,15 +403,15 @@ pub enum Expr { /// Character constant. CharConstant(char, Position), /// String constant. - StringConstant(String, Position), + StringConstant(Cow<'static, str>, Position), /// Variable access. - Variable(String, Position), + Variable(Cow<'static, str>, Position), /// Property access. - Property(String, Position), + Property(Cow<'static, str>, Position), /// { stmt } Stmt(Box, Position), /// func(expr, ... ) - FunctionCall(String, Vec, Option, Position), + FunctionCall(Cow<'static, str>, Vec, Option, Position), /// expr = expr Assignment(Box, Box, Position), /// lhs.rhs @@ -446,7 +448,7 @@ impl Expr { match self { Expr::IntegerConstant(i, _) => i.into_dynamic(), Expr::CharConstant(c, _) => c.into_dynamic(), - Expr::StringConstant(s, _) => s.into_dynamic(), + Expr::StringConstant(s, _) => s.clone().into_owned().into_dynamic(), Expr::True(_) => true.into_dynamic(), Expr::False(_) => false.into_dynamic(), Expr::Unit(_) => ().into_dynamic(), @@ -1448,42 +1450,49 @@ fn parse_paren_expr<'a>( } /// Parse a function call. -fn parse_call_expr<'a>( - id: String, +fn parse_call_expr<'a, S: Into> + Display>( + id: S, input: &mut Peekable>, begin: Position, allow_stmt_expr: bool, ) -> Result { let mut args_expr_list = Vec::new(); - // id() - if let (Token::RightParen, _) = input.peek().ok_or_else(|| { - PERR::MissingToken( - ")".into(), - format!("to close the arguments list of this function call '{}'", id), - ) - .into_err_eof() - })? { - input.next(); - return Ok(Expr::FunctionCall(id, args_expr_list, None, begin)); + match input.peek() { + //id {EOF} + None => { + return Err(PERR::MissingToken( + ")".into(), + format!("to close the arguments list of this function call '{}'", id), + ) + .into_err_eof()) + } + // id() + Some((Token::RightParen, _)) => { + input.next(); + return Ok(Expr::FunctionCall(id.into(), args_expr_list, None, begin)); + } + // id... + _ => (), } loop { args_expr_list.push(parse_expr(input, allow_stmt_expr)?); - match input.peek().ok_or_else(|| { - PERR::MissingToken( - ")".into(), - format!("to close the arguments list of this function call '{}'", id), - ) - .into_err_eof() - })? { - (Token::RightParen, _) => { - input.next(); - return Ok(Expr::FunctionCall(id, args_expr_list, None, begin)); + match input.peek() { + None => { + return Err(PERR::MissingToken( + ")".into(), + format!("to close the arguments list of this function call '{}'", id), + ) + .into_err_eof()) } - (Token::Comma, _) => (), - (_, pos) => { + Some((Token::RightParen, _)) => { + input.next(); + return Ok(Expr::FunctionCall(id.into(), args_expr_list, None, begin)); + } + Some((Token::Comma, _)) => (), + Some((_, pos)) => { return Err(PERR::MissingToken( ",".into(), format!("to separate the arguments to function call '{}'", id), @@ -1643,8 +1652,8 @@ fn parse_index_expr<'a>( } /// Parse an expression that begins with an identifier. -fn parse_ident_expr<'a>( - id: String, +fn parse_ident_expr<'a, S: Into> + Display>( + id: S, input: &mut Peekable>, begin: Position, allow_stmt_expr: bool, @@ -1661,16 +1670,16 @@ fn parse_ident_expr<'a>( let pos = *pos; input.next(); parse_index_expr( - Box::new(Expr::Variable(id, begin)), + Box::new(Expr::Variable(id.into(), begin)), input, pos, allow_stmt_expr, ) } // id - variable - Some(_) => Ok(Expr::Variable(id, begin)), + Some(_) => Ok(Expr::Variable(id.into(), begin)), // EOF - None => Ok(Expr::Variable(id, begin)), + None => Ok(Expr::Variable(id.into(), begin)), } } @@ -1851,7 +1860,7 @@ fn parse_primary<'a>( (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), (Token::StringConst(s), pos) => { can_be_indexed = true; - Ok(Expr::StringConstant(s, pos)) + Ok(Expr::StringConstant(s.into(), pos)) } (Token::Identifier(s), pos) => { can_be_indexed = true; @@ -2040,7 +2049,12 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result Result { +fn parse_op_assignment>>( + op: S, + lhs: Expr, + rhs: Expr, + pos: Position, +) -> Result { let lhs_copy = lhs.clone(); // lhs op= rhs -> lhs = op(lhs, rhs) @@ -2336,7 +2350,7 @@ fn parse_for<'a>( let expr = parse_expr(input, allow_stmt_expr)?; let body = parse_block(input, true, allow_stmt_expr)?; - Ok(Stmt::For(name, Box::new(expr), Box::new(body))) + Ok(Stmt::For(name.into(), Box::new(expr), Box::new(body))) } /// Parse a variable definition statement. @@ -2367,10 +2381,10 @@ fn parse_let<'a>( match var_type { // let name = expr - ScopeEntryType::Normal => Ok(Stmt::Let(name, Some(Box::new(init_value)), pos)), + ScopeEntryType::Normal => Ok(Stmt::Let(name.into(), Some(Box::new(init_value)), pos)), // const name = { expr:constant } ScopeEntryType::Constant if init_value.is_constant() => { - Ok(Stmt::Const(name, Box::new(init_value), pos)) + Ok(Stmt::Const(name.into(), Box::new(init_value), pos)) } // const name = expr - error ScopeEntryType::Constant => { @@ -2379,7 +2393,7 @@ fn parse_let<'a>( } } else { // let name - Ok(Stmt::Let(name, None, pos)) + Ok(Stmt::Let(name.into(), None, pos)) } } @@ -2733,7 +2747,7 @@ pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> Option { } else if value.is::() { Some(Expr::CharConstant(value.cast(), pos)) } else if value.is::() { - Some(Expr::StringConstant(value.cast(), pos)) + Some(Expr::StringConstant(value.cast::().into(), pos)) } else if value.is::() { Some(if value.cast::() { Expr::True(pos) From e204ae1a2c71f722cefddf7a7364fb99bb9d1661 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Apr 2020 17:47:34 +0800 Subject: [PATCH 02/21] Add 'in' expression. --- README.md | 11 ++ src/engine.rs | 104 +++++++++++-- src/error.rs | 7 + src/optimize.rs | 53 ++++++- src/parser.rs | 404 ++++++++++++++++++++++++++++++++---------------- src/result.rs | 6 + tests/arrays.rs | 1 + tests/maps.rs | 4 + tests/string.rs | 4 + 9 files changed, 447 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 51b6d637..0213d628 100644 --- a/README.md +++ b/README.md @@ -1039,6 +1039,11 @@ record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line // (disabled with 'no_index') record[4] = '\x58'; // 0x58 = 'X' record == "Bob X. Davis: age 42 ❤\n"; + +// Use 'in' to test if a substring (or character) exists in a string +"Davis" in record == true; +'X' in record == true; +'C' in record == false; ``` The following standard functions (defined in the standard library but excluded if [`no_stdlib`]) operate on strings: @@ -1114,6 +1119,9 @@ Examples: let y = [1, 2, 3]; // array literal with 3 elements y[1] = 42; +print(1 in y); // use 'in' to test if an item exists in the array, prints true +print(9 in y); // ... prints false + print(y[1]); // prints 42 ts.list = y; // arrays can be assigned completely (by value copy) @@ -1219,6 +1227,9 @@ print(y.a); // prints 42 print(y["baz!$@"]); // prints 123.456 - access via index notation +print("baz!$@" in y); // use 'in' to test if a property exists in the object map, prints true +print("z" in y); // ... prints false + ts.obj = y; // object maps can be assigned completely (by value copy) let foo = ts.list.a; foo == 42; diff --git a/src/engine.rs b/src/engine.rs index cd4f4b2c..e8c24034 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -290,6 +290,7 @@ impl Default for Engine<'_> { (type_name::(), "map"), (type_name::(), "string"), (type_name::(), "dynamic"), + (type_name::(), "variant"), ] .iter() .map(|(k, v)| (k.to_string(), v.to_string())) @@ -416,7 +417,7 @@ impl Engine<'_> { ) -> Result, EvalAltResult> { let spec = FnSpec { name: fn_name.into(), - args: args.iter().map(|a| Any::type_id(&**a)).collect(), + args: args.iter().map(|a| Any::type_id(*a)).collect(), }; // Search built-in's and external functions @@ -501,7 +502,7 @@ impl Engine<'_> { let spec = FnSpec { name: fn_name.into(), - args: args.iter().map(|a| Any::type_id(&**a)).collect(), + args: args.iter().map(|a| Any::type_id(*a)).collect(), }; // Argument must be a string @@ -1179,6 +1180,85 @@ impl Engine<'_> { } } + // Evaluate an 'in' expression + fn eval_in_expr( + &mut self, + scope: &mut Scope, + lhs: &Expr, + rhs: &Expr, + level: usize, + ) -> Result { + let mut lhs_value = self.eval_expr(scope, lhs, level)?; + let rhs_value = self.eval_expr(scope, rhs, level)?; + + #[cfg(not(feature = "no_index"))] + { + if rhs_value.is::() { + let mut rhs_value = rhs_value.cast::(); + let def_value = false.into_dynamic(); + let mut result = false; + + // Call the '==' operator to compare each value + for value in rhs_value.iter_mut() { + if self + .call_fn_raw( + None, + "==", + &mut [lhs_value.as_mut(), value.as_mut()], + Some(&def_value), + rhs.position(), + level, + )? + .try_cast::() + .unwrap_or(false) + { + result = true; + break; + } + } + + return Ok(result.into_dynamic()); + } + } + + #[cfg(not(feature = "no_object"))] + { + if rhs_value.is::() { + let rhs_value = rhs_value.cast::(); + + // Only allows String or char + return if lhs_value.is::() { + Ok(rhs_value + .contains_key(&lhs_value.cast::()) + .into_dynamic()) + } else if lhs_value.is::() { + Ok(rhs_value + .contains_key(&lhs_value.cast::().to_string()) + .into_dynamic()) + } else { + Err(EvalAltResult::ErrorInExpr(lhs.position())) + }; + } + } + + if rhs_value.is::() { + let rhs_value = rhs_value.cast::(); + + // Only allows String or char + return if lhs_value.is::() { + Ok(rhs_value + .contains(&lhs_value.cast::()) + .into_dynamic()) + } else if lhs_value.is::() { + Ok(rhs_value.contains(lhs_value.cast::()).into_dynamic()) + } else { + Err(EvalAltResult::ErrorInExpr(lhs.position())) + }; + } + + return Err(EvalAltResult::ErrorInExpr(rhs.position())); + } + /// Evaluate an expression fn eval_expr( &mut self, @@ -1302,7 +1382,7 @@ impl Engine<'_> { self.eval_expr(scope, item, level).map(|val| arr.push(val)) })?; - Ok(Box::new(arr)) + Ok((arr).into_dynamic()) } #[cfg(not(feature = "no_object"))] @@ -1315,7 +1395,7 @@ impl Engine<'_> { }) })?; - Ok(Box::new(map)) + Ok((map).into_dynamic()) } Expr::FunctionCall(fn_name, args_expr_list, def_val, pos) => { @@ -1445,32 +1525,34 @@ impl Engine<'_> { } } - Expr::And(lhs, rhs) => Ok(Box::new( + Expr::In(lhs, rhs, _) => self.eval_in_expr(scope, lhs.as_ref(), rhs.as_ref(), level), + + Expr::And(lhs, rhs, _) => Ok(Box::new( self - .eval_expr(scope, &*lhs, level)? + .eval_expr(scope, lhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), lhs.position()) })? && // Short-circuit using && self - .eval_expr(scope, &*rhs, level)? + .eval_expr(scope, rhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) })?, )), - Expr::Or(lhs, rhs) => Ok(Box::new( + Expr::Or(lhs, rhs, _) => Ok(Box::new( self - .eval_expr(scope, &*lhs, level)? + .eval_expr(scope, lhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), lhs.position()) })? || // Short-circuit using || self - .eval_expr(scope, &*rhs, level)? + .eval_expr(scope, rhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) @@ -1559,7 +1641,7 @@ impl Engine<'_> { // For loop Stmt::For(name, expr, body) => { let arr = self.eval_expr(scope, expr, level)?; - let tid = Any::type_id(&*arr); + let tid = Any::type_id(arr.as_ref()); if let Some(type_iterators) = &self.type_iterators { if let Some(iter_fn) = type_iterators.get(&tid) { diff --git a/src/error.rs b/src/error.rs index 0991a19b..5ff11832 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,6 +54,8 @@ pub enum ParseErrorType { /// Not available under the `no_index` feature. #[cfg(not(feature = "no_index"))] MalformedIndexExpr(String), + /// An expression in an `in` expression has syntax error. Wrapped value is the error description (if any). + MalformedInExpr(String), /// A map definition has duplicated property names. Wrapped value is the property name. /// /// Not available under the `no_object` feature. @@ -138,6 +140,7 @@ impl ParseError { ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", + ParseErrorType::MalformedInExpr(_) => "Invalid 'in' expression", #[cfg(not(feature = "no_object"))] ParseErrorType::DuplicatedProperty(_) => "Duplicated property in object map literal", ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant", @@ -180,6 +183,10 @@ impl fmt::Display for ParseError { write!(f, "{}", if s.is_empty() { self.desc() } else { s })? } + ParseErrorType::MalformedInExpr(s) => { + write!(f, "{}", if s.is_empty() { self.desc() } else { s })? + } + #[cfg(not(feature = "no_object"))] ParseErrorType::DuplicatedProperty(s) => { write!(f, "Duplicated property '{}' for object map literal", s)? diff --git a/src/optimize.rs b/src/optimize.rs index 22693977..f20e719a 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -392,8 +392,55 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { .into_iter() .map(|(key, expr, pos)| (key, optimize_expr(expr, state), pos)) .collect(), pos), + // lhs in rhs + Expr::In(lhs, rhs, pos) => match (*lhs, *rhs) { + // "xxx" in "xxxxx" + (Expr::StringConstant(lhs, pos), Expr::StringConstant(rhs, _)) => { + state.set_dirty(); + if rhs.contains(lhs.as_ref()) { + Expr::True(pos) + } else { + Expr::False(pos) + } + } + // 'x' in "xxxxx" + (Expr::CharConstant(lhs, pos), Expr::StringConstant(rhs, _)) => { + state.set_dirty(); + if rhs.contains(&lhs.to_string()) { + Expr::True(pos) + } else { + Expr::False(pos) + } + } + // "xxx" in #{...} + (Expr::StringConstant(lhs, pos), Expr::Map(items, _)) => { + state.set_dirty(); + if items.iter().find(|(name, _, _)| name == &lhs).is_some() { + Expr::True(pos) + } else { + Expr::False(pos) + } + } + // 'x' in #{...} + (Expr::CharConstant(lhs, pos), Expr::Map(items, _)) => { + state.set_dirty(); + let lhs = lhs.to_string(); + + if items.iter().find(|(name, _, _)| name == &lhs).is_some() { + Expr::True(pos) + } else { + Expr::False(pos) + } + } + // lhs in rhs + (lhs, rhs) => Expr::In( + Box::new(optimize_expr(lhs, state)), + Box::new(optimize_expr(rhs, state)), + pos + ), + }, // lhs && rhs - Expr::And(lhs, rhs) => match (*lhs, *rhs) { + Expr::And(lhs, rhs, pos) => match (*lhs, *rhs) { // true && rhs -> rhs (Expr::True(_), rhs) => { state.set_dirty(); @@ -413,10 +460,11 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { (lhs, rhs) => Expr::And( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), + pos ), }, // lhs || rhs - Expr::Or(lhs, rhs) => match (*lhs, *rhs) { + Expr::Or(lhs, rhs, pos) => match (*lhs, *rhs) { // false || rhs -> rhs (Expr::False(_), rhs) => { state.set_dirty(); @@ -436,6 +484,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { (lhs, rhs) => Expr::Or( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), + pos ), }, diff --git a/src/parser.rs b/src/parser.rs index b592af43..e425dc4b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -420,16 +420,18 @@ pub enum Expr { /// expr[expr] #[cfg(not(feature = "no_index"))] Index(Box, Box, Position), - #[cfg(not(feature = "no_index"))] /// [ expr, ... ] + #[cfg(not(feature = "no_index"))] Array(Vec, Position), - #[cfg(not(feature = "no_object"))] /// #{ name:expr, ... } + #[cfg(not(feature = "no_object"))] Map(Vec<(String, Expr, Position)>, Position), + /// lhs in rhs + In(Box, Box, Position), /// lhs && rhs - And(Box, Box), + And(Box, Box, Position), /// lhs || rhs - Or(Box, Box), + Or(Box, Box, Position), /// true True(Position), /// false @@ -446,29 +448,29 @@ impl Expr { /// Panics when the expression is not constant. 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.clone().into_owned().into_dynamic(), - Expr::True(_) => true.into_dynamic(), - Expr::False(_) => false.into_dynamic(), - Expr::Unit(_) => ().into_dynamic(), + Self::IntegerConstant(i, _) => i.into_dynamic(), + Self::CharConstant(c, _) => c.into_dynamic(), + Self::StringConstant(s, _) => s.clone().into_owned().into_dynamic(), + Self::True(_) => true.into_dynamic(), + Self::False(_) => false.into_dynamic(), + Self::Unit(_) => ().into_dynamic(), #[cfg(not(feature = "no_index"))] - Expr::Array(items, _) if items.iter().all(Expr::is_constant) => items + Self::Array(items, _) if items.iter().all(Self::is_constant) => items .iter() - .map(Expr::get_constant_value) + .map(Self::get_constant_value) .collect::>() .into_dynamic(), #[cfg(not(feature = "no_object"))] - Expr::Map(items, _) if items.iter().all(|(_, v, _)| v.is_constant()) => items + Self::Map(items, _) if items.iter().all(|(_, v, _)| v.is_constant()) => items .iter() .map(|(k, v, _)| (k.clone(), v.get_constant_value())) .collect::>() .into_dynamic(), #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(f, _) => f.into_dynamic(), + Self::FloatConstant(f, _) => f.into_dynamic(), _ => panic!("cannot get value of non-constant expression"), } @@ -481,18 +483,18 @@ impl Expr { /// Panics when the expression is not constant. pub fn get_constant_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(), + Self::IntegerConstant(i, _) => i.to_string(), + Self::CharConstant(c, _) => c.to_string(), + Self::StringConstant(_, _) => "string".to_string(), + Self::True(_) => "true".to_string(), + Self::False(_) => "false".to_string(), + Self::Unit(_) => "()".to_string(), #[cfg(not(feature = "no_index"))] - Expr::Array(items, _) if items.iter().all(Expr::is_constant) => "array".to_string(), + Self::Array(items, _) if items.iter().all(Self::is_constant) => "array".to_string(), #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(f, _) => f.to_string(), + Self::FloatConstant(f, _) => f.to_string(), _ => panic!("cannot get value of non-constant expression"), } @@ -501,35 +503,36 @@ impl Expr { /// Get the `Position` of the expression. pub fn position(&self) -> Position { match self { - Expr::IntegerConstant(_, pos) - | Expr::CharConstant(_, pos) - | Expr::StringConstant(_, pos) - | Expr::Variable(_, pos) - | Expr::Property(_, pos) - | Expr::Stmt(_, pos) - | Expr::FunctionCall(_, _, _, pos) - | Expr::True(pos) - | Expr::False(pos) - | Expr::Unit(pos) => *pos, + Self::IntegerConstant(_, pos) + | Self::CharConstant(_, pos) + | Self::StringConstant(_, pos) + | Self::Variable(_, pos) + | Self::Property(_, pos) + | Self::Stmt(_, pos) + | Self::FunctionCall(_, _, _, pos) + | Self::And(_, _, pos) + | Self::Or(_, _, pos) + | Self::In(_, _, pos) + | Self::True(pos) + | Self::False(pos) + | Self::Unit(pos) => *pos, - Expr::Assignment(expr, _, _) | Expr::And(expr, _) | Expr::Or(expr, _) => { - expr.position() - } + Self::Assignment(expr, _, _) => expr.position(), #[cfg(not(feature = "no_object"))] - Expr::Dot(expr, _, _) => expr.position(), + Self::Dot(expr, _, _) => expr.position(), #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(_, pos) => *pos, + Self::FloatConstant(_, pos) => *pos, #[cfg(not(feature = "no_index"))] - Expr::Array(_, pos) => *pos, + Self::Array(_, pos) => *pos, #[cfg(not(feature = "no_object"))] - Expr::Map(_, pos) => *pos, + Self::Map(_, pos) => *pos, #[cfg(not(feature = "no_index"))] - Expr::Index(expr, _, _) => expr.position(), + Self::Index(expr, _, _) => expr.position(), } } @@ -539,35 +542,48 @@ impl Expr { pub fn is_pure(&self) -> bool { match self { #[cfg(not(feature = "no_index"))] - Expr::Array(expressions, _) => expressions.iter().all(Expr::is_pure), + Self::Array(expressions, _) => expressions.iter().all(Self::is_pure), #[cfg(not(feature = "no_index"))] - Expr::Index(x, y, _) => x.is_pure() && y.is_pure(), + Self::Index(x, y, _) => x.is_pure() && y.is_pure(), - Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(), + Self::And(x, y, _) | Self::Or(x, y, _) | Self::In(x, y, _) => { + x.is_pure() && y.is_pure() + } - Expr::Stmt(stmt, _) => stmt.is_pure(), + Self::Stmt(stmt, _) => stmt.is_pure(), - expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)), + expr => expr.is_constant() || matches!(expr, Self::Variable(_, _)), } } /// Is the expression a constant? pub fn is_constant(&self) -> bool { match self { - Expr::IntegerConstant(_, _) - | Expr::CharConstant(_, _) - | Expr::StringConstant(_, _) - | Expr::True(_) - | Expr::False(_) - | Expr::Unit(_) => true, + Self::IntegerConstant(_, _) + | Self::CharConstant(_, _) + | Self::StringConstant(_, _) + | Self::True(_) + | Self::False(_) + | Self::Unit(_) => true, #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(_, _) => true, + Self::FloatConstant(_, _) => true, // An array literal is constant if all items are constant #[cfg(not(feature = "no_index"))] - Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant), + Self::Array(expressions, _) => expressions.iter().all(Self::is_constant), + + // An map literal is constant if all items are constant + #[cfg(not(feature = "no_object"))] + Self::Map(items, _) => items.iter().map(|(_, expr, _)| expr).all(Self::is_constant), + + // Check in expression + Self::In(lhs, rhs, _) => match (lhs.as_ref(), rhs.as_ref()) { + (Self::StringConstant(_, _), Self::StringConstant(_, _)) + | (Self::CharConstant(_, _), Self::StringConstant(_, _)) => true, + _ => false, + }, _ => false, } @@ -653,7 +669,7 @@ pub enum Token { impl Token { /// Get the syntax of the token. pub fn syntax(&self) -> Cow { - use self::Token::*; + use Token::*; match self { IntegerConstant(i) => i.to_string().into(), @@ -738,7 +754,7 @@ impl Token { // If another operator is after these, it's probably an unary operator // (not sure about fn name). pub fn is_next_unary(&self) -> bool { - use self::Token::*; + use Token::*; match self { LexError(_) | @@ -799,40 +815,31 @@ impl Token { /// Get the precedence number of the token. pub fn precedence(&self) -> u8 { + use Token::*; + match self { - Self::Equals - | Self::PlusAssign - | Self::MinusAssign - | Self::MultiplyAssign - | Self::DivideAssign - | Self::LeftShiftAssign - | Self::RightShiftAssign - | Self::AndAssign - | Self::OrAssign - | Self::XOrAssign - | Self::ModuloAssign - | Self::PowerOfAssign => 10, + Equals | PlusAssign | MinusAssign | MultiplyAssign | DivideAssign | LeftShiftAssign + | RightShiftAssign | AndAssign | OrAssign | XOrAssign | ModuloAssign + | PowerOfAssign => 10, - Self::Or | Self::XOr | Self::Pipe => 50, + Or | XOr | Pipe => 40, - Self::And | Self::Ampersand => 60, + And | Ampersand => 50, - Self::LessThan - | Self::LessThanEqualsTo - | Self::GreaterThan - | Self::GreaterThanEqualsTo - | Self::EqualsTo - | Self::NotEqualsTo => 70, + LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo | EqualsTo + | NotEqualsTo => 60, - Self::Plus | Self::Minus => 80, + In => 70, - Self::Divide | Self::Multiply | Self::PowerOf => 90, + Plus | Minus => 80, - Self::LeftShift | Self::RightShift => 100, + Divide | Multiply | PowerOf => 90, - Self::Modulo => 110, + LeftShift | RightShift => 100, - Self::Period => 120, + Modulo => 110, + + Period => 120, _ => 0, } @@ -840,23 +847,16 @@ impl Token { /// Does an expression bind to the right (instead of left)? pub fn is_bind_right(&self) -> bool { + use Token::*; + match self { // Assignments bind to the right - Self::Equals - | Self::PlusAssign - | Self::MinusAssign - | Self::MultiplyAssign - | Self::DivideAssign - | Self::LeftShiftAssign - | Self::RightShiftAssign - | Self::AndAssign - | Self::OrAssign - | Self::XOrAssign - | Self::ModuloAssign - | Self::PowerOfAssign => true, + Equals | PlusAssign | MinusAssign | MultiplyAssign | DivideAssign | LeftShiftAssign + | RightShiftAssign | AndAssign | OrAssign | XOrAssign | ModuloAssign + | PowerOfAssign => true, // Property access binds to the right - Self::Period => true, + Period => true, _ => false, } @@ -1539,22 +1539,18 @@ fn parse_index_expr<'a>( Expr::FloatConstant(_, pos) | Expr::CharConstant(_, pos) | Expr::Assignment(_, _, pos) - | Expr::Unit(pos) + | Expr::And(_, _, pos) + | Expr::Or(_, _, pos) + | Expr::In(_, _, pos) | Expr::True(pos) - | Expr::False(pos) => { + | Expr::False(pos) + | Expr::Unit(pos) => { return Err(PERR::MalformedIndexExpr( "Only arrays, object maps and strings can be indexed".into(), ) .into_err(pos)) } - Expr::And(lhs, _) | Expr::Or(lhs, _) => { - return Err(PERR::MalformedIndexExpr( - "Only arrays, object maps and strings can be indexed".into(), - ) - .into_err(lhs.position())) - } - _ => (), }, @@ -1572,22 +1568,18 @@ fn parse_index_expr<'a>( Expr::FloatConstant(_, pos) | Expr::CharConstant(_, pos) | Expr::Assignment(_, _, pos) - | Expr::Unit(pos) + | Expr::And(_, _, pos) + | Expr::Or(_, _, pos) + | Expr::In(_, _, pos) | Expr::True(pos) - | Expr::False(pos) => { + | Expr::False(pos) + | Expr::Unit(pos) => { return Err(PERR::MalformedIndexExpr( "Only arrays, object maps and strings can be indexed".into(), ) .into_err(pos)) } - Expr::And(lhs, _) | Expr::Or(lhs, _) => { - return Err(PERR::MalformedIndexExpr( - "Only arrays, object maps and strings can be indexed".into(), - ) - .into_err(lhs.position())) - } - _ => (), }, @@ -1613,15 +1605,12 @@ fn parse_index_expr<'a>( ) .into_err(*pos)) } - // lhs[??? && ???], lhs[??? || ???] - Expr::And(lhs, _) | Expr::Or(lhs, _) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not a boolean".into(), - ) - .into_err(lhs.position())) - } - // lhs[true], lhs[false] - Expr::True(pos) | Expr::False(pos) => { + // lhs[??? && ???], lhs[??? || ???], lhs[??? in ???], lhs[true], lhs[false] + Expr::And(_, _, pos) + | Expr::Or(_, _, pos) + | Expr::In(_, _, pos) + | Expr::True(pos) + | Expr::False(pos) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a boolean".into(), ) @@ -1699,9 +1688,7 @@ fn parse_array_literal<'a>( match input.peek().ok_or_else(|| { PERR::MissingToken("]".into(), "to end this array literal".into()).into_err_eof() })? { - (Token::Comma, _) => { - input.next(); - } + (Token::Comma, _) => input.next(), (Token::RightBracket, _) => break, (_, pos) => { return Err(PERR::MissingToken( @@ -1710,7 +1697,7 @@ fn parse_array_literal<'a>( ) .into_err(*pos)) } - } + }; } } @@ -2065,6 +2052,154 @@ fn parse_op_assignment>>( ) } +/// Parse an 'in' expression. +fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { + match (&lhs, &rhs) { + #[cfg(not(feature = "no_float"))] + (_, Expr::FloatConstant(_, pos)) => { + return Err(PERR::MalformedInExpr( + "'in' expression expects a string, array or object map".into(), + ) + .into_err(*pos)) + } + + (_, Expr::IntegerConstant(_, pos)) + | (_, Expr::And(_, _, pos)) + | (_, Expr::Or(_, _, pos)) + | (_, Expr::In(_, _, pos)) + | (_, Expr::True(pos)) + | (_, Expr::False(pos)) + | (_, Expr::Assignment(_, _, pos)) + | (_, Expr::Unit(pos)) => { + return Err(PERR::MalformedInExpr( + "'in' expression expects a string, array or object map".into(), + ) + .into_err(*pos)) + } + + // "xxx" in "xxxx", 'x' in "xxxx" - OK! + (Expr::StringConstant(_, _), Expr::StringConstant(_, _)) + | (Expr::CharConstant(_, _), Expr::StringConstant(_, _)) => (), + + // 123.456 in "xxxx" + #[cfg(not(feature = "no_float"))] + (Expr::FloatConstant(_, pos), Expr::StringConstant(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for a string expects a string, not a float".into(), + ) + .into_err(*pos)) + } + // 123 in "xxxx" + (Expr::IntegerConstant(_, pos), Expr::StringConstant(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for a string expects a string, not a number".into(), + ) + .into_err(*pos)) + } + // (??? && ???) in "xxxx", (??? || ???) in "xxxx", (??? in ???) in "xxxx", + // true in "xxxx", false in "xxxx" + (Expr::And(_, _, pos), Expr::StringConstant(_, _)) + | (Expr::Or(_, _, pos), Expr::StringConstant(_, _)) + | (Expr::In(_, _, pos), Expr::StringConstant(_, _)) + | (Expr::True(pos), Expr::StringConstant(_, _)) + | (Expr::False(pos), Expr::StringConstant(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for a string expects a string, not a boolean".into(), + ) + .into_err(*pos)) + } + // [???, ???, ???] in "xxxx" + #[cfg(not(feature = "no_index"))] + (Expr::Array(_, pos), Expr::StringConstant(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for a string expects a string, not an array".into(), + ) + .into_err(*pos)) + } + // #{...} in "xxxx" + #[cfg(not(feature = "no_object"))] + (Expr::Map(_, pos), Expr::StringConstant(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for a string expects a string, not an object map".into(), + ) + .into_err(*pos)) + } + // (??? = ???) in "xxxx", () in "xxxx" + (Expr::Assignment(_, _, pos), Expr::StringConstant(_, _)) + | (Expr::Unit(pos), Expr::StringConstant(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for a string expects a string, not ()".into(), + ) + .into_err(*pos)) + } + + // "xxx" in #{...}, 'x' in #{...} - OK! + #[cfg(not(feature = "no_object"))] + (Expr::StringConstant(_, _), Expr::Map(_, _)) + | (Expr::CharConstant(_, _), Expr::Map(_, _)) => (), + + // 123.456 in #{...} + #[cfg(not(feature = "no_float"))] + #[cfg(not(feature = "no_object"))] + (Expr::FloatConstant(_, pos), Expr::Map(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for an object map expects a string, not a float".into(), + ) + .into_err(*pos)) + } + // 123 in #{...} + #[cfg(not(feature = "no_object"))] + (Expr::IntegerConstant(_, pos), Expr::Map(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for an object map expects a string, not a number".into(), + ) + .into_err(*pos)) + } + // (??? && ???) in #{...}, (??? || ???) in #{...}, (??? in ???) in #{...}, + // true in #{...}, false in #{...} + #[cfg(not(feature = "no_object"))] + (Expr::And(_, _, pos), Expr::Map(_, _)) + | (Expr::Or(_, _, pos), Expr::Map(_, _)) + | (Expr::In(_, _, pos), Expr::Map(_, _)) + | (Expr::True(pos), Expr::Map(_, _)) + | (Expr::False(pos), Expr::Map(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for an object map expects a string, not a boolean".into(), + ) + .into_err(*pos)) + } + // [???, ???, ???] in #{..} + #[cfg(not(feature = "no_index"))] + #[cfg(not(feature = "no_object"))] + (Expr::Array(_, pos), Expr::Map(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for an object map expects a string, not an array".into(), + ) + .into_err(*pos)) + } + // #{...} in #{..} + #[cfg(not(feature = "no_object"))] + (Expr::Map(_, pos), Expr::Map(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for an object map expects a string, not an object map".into(), + ) + .into_err(*pos)) + } + // (??? = ???) in #{...}, () in #{...} + #[cfg(not(feature = "no_object"))] + (Expr::Assignment(_, _, pos), Expr::Map(_, _)) | (Expr::Unit(pos), Expr::Map(_, _)) => { + return Err(PERR::MalformedInExpr( + "'in' expression for an object map expects a string, not ()".into(), + ) + .into_err(*pos)) + } + + _ => (), + } + + Ok(Expr::In(Box::new(lhs), Box::new(rhs), op_pos)) +} + /// Parse a binary expression. fn parse_binary_op<'a>( input: &mut Peekable>, @@ -2189,8 +2324,11 @@ fn parse_binary_op<'a>( pos, ), - Token::Or => Expr::Or(Box::new(current_lhs), Box::new(rhs)), - Token::And => Expr::And(Box::new(current_lhs), Box::new(rhs)), + Token::Or => Expr::Or(Box::new(current_lhs), Box::new(rhs), pos), + Token::And => Expr::And(Box::new(current_lhs), Box::new(rhs), pos), + + Token::In => parse_in_expr(current_lhs, rhs, pos)?, + Token::XOr => Expr::FunctionCall("^".into(), vec![current_lhs, rhs], None, pos), Token::OrAssign => parse_op_assignment("|", current_lhs, rhs, pos)?, Token::AndAssign => parse_op_assignment("&", current_lhs, rhs, pos)?, @@ -2561,11 +2699,9 @@ fn parse_fn<'a>( .peek() .ok_or_else(|| PERR::FnMissingParams(name.clone()).into_err_eof())? { - (Token::LeftParen, _) => { - input.next(); - } + (Token::LeftParen, _) => input.next(), (_, pos) => return Err(PERR::FnMissingParams(name).into_err(*pos)), - } + }; let mut params = Vec::new(); @@ -2580,9 +2716,7 @@ fn parse_fn<'a>( .next() .ok_or_else(|| PERR::MissingToken(")".into(), end_err.to_string()).into_err_eof())? { - (Token::Identifier(s), pos) => { - params.push((s, pos)); - } + (Token::Identifier(s), pos) => params.push((s, pos)), (_, pos) => return Err(PERR::MissingToken(")".into(), end_err).into_err(pos)), } @@ -2622,9 +2756,11 @@ fn parse_fn<'a>( None => return Err(PERR::FnMissingBody(name).into_err_eof()), }; + let params = params.into_iter().map(|(p, _)| p).collect(); + Ok(FnDef { name, - params: params.into_iter().map(|(p, _)| p).collect(), + params, body, pos, }) diff --git a/src/result.rs b/src/result.rs index 57594ccc..b9f57f86 100644 --- a/src/result.rs +++ b/src/result.rs @@ -49,6 +49,8 @@ pub enum EvalAltResult { ErrorNumericIndexExpr(Position), /// Trying to index into a map with an index that is not `String`. ErrorStringIndexExpr(Position), + /// Invalid arguments for `in` operator. + ErrorInExpr(Position), /// The guard expression in an `if` or `while` statement does not return a boolean value. ErrorLogicGuard(Position), /// The `for` statement encounters a type that is not an iterator. @@ -118,6 +120,7 @@ impl EvalAltResult { } Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable", Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect", + Self::ErrorInExpr(_) => "Malformed 'in' expression", Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorStackOverflow(_) => "Stack overflow", @@ -154,6 +157,7 @@ impl fmt::Display for EvalAltResult { | Self::ErrorLogicGuard(pos) | Self::ErrorFor(pos) | Self::ErrorAssignmentToUnknownLHS(pos) + | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorStackOverflow(pos) => write!(f, "{} ({})", desc, pos), @@ -256,6 +260,7 @@ impl EvalAltResult { | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToConstant(_, pos) | Self::ErrorMismatchOutputType(_, pos) + | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) | Self::ErrorStackOverflow(pos) @@ -288,6 +293,7 @@ impl EvalAltResult { | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToConstant(_, pos) | Self::ErrorMismatchOutputType(_, pos) + | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) | Self::ErrorStackOverflow(pos) diff --git a/tests/arrays.rs b/tests/arrays.rs index 1a7517ae..438ae4ea 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -11,6 +11,7 @@ fn test_arrays() -> Result<(), EvalAltResult> { engine.eval::(r#"let y = [1, [ 42, 88, "93" ], 3]; y[1][2][1]"#)?, '3' ); + assert!(engine.eval::("let y = [1, 2, 3]; 2 in y")?); #[cfg(not(feature = "no_stdlib"))] { diff --git a/tests/maps.rs b/tests/maps.rs index 5ed42519..746be5cd 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -29,6 +29,10 @@ fn test_map_indexing() -> Result<(), EvalAltResult> { ); engine.eval::<()>("let y = #{a: 1, b: 2, c: 3}; y.z")?; + assert!(engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "c" in y"#)?); + assert!(engine.eval::("let y = #{a: 1, b: 2, c: 3}; 'b' in y")?); + assert!(!engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "z" in y"#)?); + #[cfg(not(feature = "no_stdlib"))] { assert_eq!( diff --git a/tests/string.rs b/tests/string.rs index 92310e33..1ffd2e2e 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -15,6 +15,10 @@ fn test_string() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::(r#""foo" + "bar""#)?, "foobar"); + assert!(engine.eval::(r#"let y = "hello, world!"; "world" in y"#)?); + assert!(engine.eval::(r#"let y = "hello, world!"; 'w' in y"#)?); + assert!(!engine.eval::(r#"let y = "hello, world!"; "hey" in y"#)?); + #[cfg(not(feature = "no_stdlib"))] assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); From e795a50ae20a00c9b7bfb04db998de4e20c132ac Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 7 Apr 2020 13:23:06 +0800 Subject: [PATCH 03/21] Make Engine reentrant to prepare for parallel execution. --- README.md | 19 +- examples/hello.rs | 2 +- examples/no_std.rs | 2 +- examples/reuse_scope.rs | 2 +- src/api.rs | 204 +++++++++---------- src/engine.rs | 384 ++++++++++++++++++------------------ src/lib.rs | 2 +- src/optimize.rs | 37 ++-- src/parser.rs | 2 +- src/result.rs | 2 + src/scope.rs | 2 +- tests/arrays.rs | 2 +- tests/binary_ops.rs | 2 +- tests/bit_shift.rs | 4 +- tests/bool_op.rs | 12 +- tests/call_fn.rs | 2 +- tests/chars.rs | 2 +- tests/comments.rs | 4 +- tests/compound_equality.rs | 16 +- tests/constants.rs | 2 +- tests/decrement.rs | 2 +- tests/eval.rs | 6 +- tests/expressions.rs | 2 +- tests/float.rs | 2 +- tests/for.rs | 4 +- tests/if_block.rs | 4 +- tests/increment.rs | 2 +- tests/internal_fn.rs | 15 +- tests/looping.rs | 2 +- tests/maps.rs | 8 +- tests/math.rs | 2 +- tests/mismatched_op.rs | 2 +- tests/not.rs | 2 +- tests/number_literals.rs | 8 +- tests/ops.rs | 6 +- tests/power_of.rs | 4 +- tests/side_effects.rs | 24 ++- tests/string.rs | 2 +- tests/throw.rs | 2 +- tests/unary_after_binary.rs | 2 +- tests/unary_minus.rs | 2 +- tests/unit.rs | 6 +- tests/var_scope.rs | 4 +- tests/while_loop.rs | 2 +- 44 files changed, 415 insertions(+), 404 deletions(-) diff --git a/README.md b/README.md index 0213d628..4b5c239e 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ use rhai::{Engine, EvalAltResult}; fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let result = engine.eval::("40 + 2")?; @@ -446,7 +446,7 @@ fn get_an_any() -> Dynamic { fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); engine.register_fn("add", add); @@ -497,7 +497,7 @@ fn show_it(x: &mut T) -> () { fn main() { - let mut engine = Engine::new(); + let engine = Engine::new(); engine.register_fn("print", show_it as fn(x: &mut i64)->()); engine.register_fn("print", show_it as fn(x: &mut bool)->()); @@ -533,7 +533,7 @@ fn safe_divide(x: i64, y: i64) -> Result { fn main() { - let mut engine = Engine::new(); + let engine = Engine::new(); // Fallible functions that return Result values must use register_result_fn() engine.register_result_fn("divide", safe_divide); @@ -584,7 +584,7 @@ impl TestStruct { fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); engine.register_type::(); @@ -622,7 +622,7 @@ impl TestStruct { } } -let mut engine = Engine::new(); +let engine = Engine::new(); engine.register_type::(); ``` @@ -712,7 +712,7 @@ impl TestStruct { } } -let mut engine = Engine::new(); +let engine = Engine::new(); engine.register_type::(); @@ -748,7 +748,7 @@ use rhai::{Engine, Scope, EvalAltResult}; fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); // First create the state let mut scope = Scope::new(); @@ -1885,7 +1885,8 @@ print("z = " + z); // <- error: variable 'z' not found Script segments passed to `eval` execute inside the current [`Scope`], so they can access and modify _everything_, including all variables that are visible at that position in code! It is almost as if the script segments were -physically pasted in at the position of the `eval` call. +physically pasted in at the position of the `eval` call. But because of this, new functions cannot be defined +within an `eval` call, since functions can only be defined at the global level, not inside a function call! ```rust let script = "x += 32"; diff --git a/examples/hello.rs b/examples/hello.rs index 8527d466..c72a38b5 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,7 +1,7 @@ use rhai::{Engine, EvalAltResult, INT}; fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let result = engine.eval::("40 + 2")?; diff --git a/examples/no_std.rs b/examples/no_std.rs index 32cf14a2..ad9d9a8a 100644 --- a/examples/no_std.rs +++ b/examples/no_std.rs @@ -3,7 +3,7 @@ use rhai::{Engine, EvalAltResult, INT}; fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let result = engine.eval::("40 + 2")?; diff --git a/examples/reuse_scope.rs b/examples/reuse_scope.rs index 549ccea1..ab936656 100644 --- a/examples/reuse_scope.rs +++ b/examples/reuse_scope.rs @@ -1,7 +1,7 @@ use rhai::{Engine, EvalAltResult, Scope, INT}; fn main() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let mut scope = Scope::new(); engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; diff --git a/src/api.rs b/src/api.rs index 047cf525..461c97e9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -334,7 +334,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Compile a script to an AST and store it for later evaluation /// let ast = engine.compile("40 + 2")?; @@ -413,7 +413,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Compile a script file to an AST and store it for later evaluation. /// // Notice that a PathBuf is required which can easily be constructed from a string. @@ -481,7 +481,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Compile a script to an AST and store it for later evaluation /// let ast = engine.compile_expression("40 + 2")?; @@ -552,7 +552,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Notice that a PathBuf is required which can easily be constructed from a string. /// let result = engine.eval_file::("script.rhai".into())?; @@ -560,7 +560,7 @@ impl<'e> Engine<'e> { /// # } /// ``` #[cfg(not(feature = "no_std"))] - pub fn eval_file(&mut self, path: PathBuf) -> Result { + pub fn eval_file(&self, path: PathBuf) -> Result { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } @@ -572,7 +572,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Create initialized scope /// let mut scope = Scope::new(); @@ -585,7 +585,7 @@ impl<'e> Engine<'e> { /// ``` #[cfg(not(feature = "no_std"))] pub fn eval_file_with_scope( - &mut self, + &self, scope: &mut Scope, path: PathBuf, ) -> Result { @@ -600,13 +600,13 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// assert_eq!(engine.eval::("40 + 2")?, 42); /// # Ok(()) /// # } /// ``` - pub fn eval(&mut self, input: &str) -> Result { + pub fn eval(&self, input: &str) -> Result { self.eval_with_scope(&mut Scope::new(), input) } @@ -618,7 +618,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Create initialized scope /// let mut scope = Scope::new(); @@ -633,7 +633,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn eval_with_scope( - &mut self, + &self, scope: &mut Scope, input: &str, ) -> Result { @@ -649,13 +649,13 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// assert_eq!(engine.eval_expression::("40 + 2")?, 42); /// # Ok(()) /// # } /// ``` - pub fn eval_expression(&mut self, input: &str) -> Result { + pub fn eval_expression(&self, input: &str) -> Result { self.eval_expression_with_scope(&mut Scope::new(), input) } @@ -667,7 +667,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Create initialized scope /// let mut scope = Scope::new(); @@ -678,7 +678,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn eval_expression_with_scope( - &mut self, + &self, scope: &mut Scope, input: &str, ) -> Result { @@ -697,7 +697,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Compile a script to an AST and store it for later evaluation /// let ast = engine.compile("40 + 2")?; @@ -707,7 +707,7 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` - pub fn eval_ast(&mut self, ast: &AST) -> Result { + pub fn eval_ast(&self, ast: &AST) -> Result { self.eval_ast_with_scope(&mut Scope::new(), ast) } @@ -719,7 +719,7 @@ impl<'e> Engine<'e> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// // Compile a script to an AST and store it for later evaluation /// let ast = engine.compile("x + 2")?; @@ -741,7 +741,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn eval_ast_with_scope( - &mut self, + &self, scope: &mut Scope, ast: &AST, ) -> Result { @@ -756,32 +756,25 @@ impl<'e> Engine<'e> { } pub(crate) fn eval_ast_with_scope_raw( - &mut self, + &self, scope: &mut Scope, ast: &AST, ) -> Result { - let statements = { - let AST(statements, functions) = ast; - self.fn_lib = Some(functions.clone()); - statements - }; - - let result = statements + ast.0 .iter() - .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt, 0)); - - self.fn_lib = None; - - result.or_else(|err| match err { - EvalAltResult::Return(out, _) => Ok(out), - _ => Err(err), - }) + .try_fold(().into_dynamic(), |_, stmt| { + self.eval_stmt(scope, Some(ast.1.as_ref()), stmt, 0) + }) + .or_else(|err| match err { + EvalAltResult::Return(out, _) => Ok(out), + _ => Err(err), + }) } /// Evaluate a file, 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. #[cfg(not(feature = "no_std"))] - pub fn consume_file(&mut self, path: PathBuf) -> Result<(), EvalAltResult> { + pub fn consume_file(&self, path: PathBuf) -> Result<(), EvalAltResult> { Self::read_file(path).and_then(|contents| self.consume(&contents)) } @@ -789,7 +782,7 @@ impl<'e> Engine<'e> { /// Useful for when you don't need the result, but still need to keep track of possible errors. #[cfg(not(feature = "no_std"))] pub fn consume_file_with_scope( - &mut self, + &self, scope: &mut Scope, path: PathBuf, ) -> Result<(), EvalAltResult> { @@ -798,17 +791,13 @@ impl<'e> Engine<'e> { /// Evaluate a string, 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. - pub fn consume(&mut self, input: &str) -> Result<(), EvalAltResult> { + pub fn consume(&self, input: &str) -> Result<(), EvalAltResult> { self.consume_with_scope(&mut Scope::new(), input) } /// 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. - pub fn consume_with_scope( - &mut self, - scope: &mut Scope, - input: &str, - ) -> Result<(), EvalAltResult> { + pub fn consume_with_scope(&self, scope: &mut Scope, input: &str) -> Result<(), EvalAltResult> { let tokens_stream = lex(input); let ast = parse(&mut tokens_stream.peekable(), self, scope) @@ -819,33 +808,27 @@ impl<'e> Engine<'e> { /// Evaluate an AST, 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. - pub fn consume_ast(&mut self, ast: &AST) -> Result<(), EvalAltResult> { + pub fn consume_ast(&self, ast: &AST) -> Result<(), EvalAltResult> { self.consume_ast_with_scope(&mut Scope::new(), 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. pub fn consume_ast_with_scope( - &mut self, + &self, scope: &mut Scope, ast: &AST, ) -> Result<(), EvalAltResult> { - let statements = { - let AST(statements, functions) = ast; - self.fn_lib = Some(functions.clone()); - statements - }; - - let result = statements + ast.0 .iter() - .try_fold(().into_dynamic(), |_, stmt| self.eval_stmt(scope, stmt, 0)); - - self.fn_lib = None; - - result.map(|_| ()).or_else(|err| match err { - EvalAltResult::Return(_, _) => Ok(()), - _ => Err(err), - }) + .try_fold(().into_dynamic(), |_, stmt| { + self.eval_stmt(scope, Some(ast.1.as_ref()), stmt, 0) + }) + .map(|_| ()) + .or_else(|err| match err { + EvalAltResult::Return(_, _) => Ok(()), + _ => Err(err), + }) } /// Call a script function defined in an `AST` with no argument. @@ -859,7 +842,7 @@ impl<'e> Engine<'e> { /// # { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// let ast = engine.compile("fn num() { 42 + foo }")?; /// @@ -876,7 +859,7 @@ impl<'e> Engine<'e> { /// ``` #[cfg(not(feature = "no_function"))] pub fn call_fn0( - &mut self, + &self, scope: &mut Scope, ast: &AST, name: &str, @@ -895,7 +878,7 @@ impl<'e> Engine<'e> { /// # { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// let ast = engine.compile("fn inc(x) { x + foo }")?; /// @@ -912,7 +895,7 @@ impl<'e> Engine<'e> { /// ``` #[cfg(not(feature = "no_function"))] pub fn call_fn1( - &mut self, + &self, scope: &mut Scope, ast: &AST, name: &str, @@ -932,7 +915,7 @@ impl<'e> Engine<'e> { /// # { /// use rhai::{Engine, Scope}; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// let ast = engine.compile("fn add(x, y) { len(x) + y + foo }")?; /// @@ -949,7 +932,7 @@ impl<'e> Engine<'e> { /// ``` #[cfg(not(feature = "no_function"))] pub fn call_fn( - &mut self, + &self, scope: &mut Scope, ast: &AST, name: &str, @@ -960,29 +943,24 @@ impl<'e> Engine<'e> { #[cfg(not(feature = "no_function"))] fn call_fn_internal( - &mut self, + &self, scope: &mut Scope, ast: &AST, name: &str, mut arg_values: Vec, ) -> Result { let mut args: Vec<_> = arg_values.iter_mut().map(Dynamic::as_mut).collect(); + let fn_lib = Some(ast.1.as_ref()); + let pos = Position::none(); - self.fn_lib = Some(ast.1.clone()); - - let result = self - .call_fn_raw(Some(scope), name, &mut args, None, Position::none(), 0)? + self.call_fn_raw(Some(scope), fn_lib, name, &mut args, None, pos, 0)? .try_cast() .map_err(|a| { EvalAltResult::ErrorMismatchOutputType( self.map_type_name((*a).type_name()).into(), - Position::none(), + pos, ) - }); - - self.fn_lib = None; - - result + }) } /// Optimize the `AST` with constants defined in an external Scope. @@ -998,12 +976,8 @@ impl<'e> Engine<'e> { /// (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_into_ast( - self, - scope, - ast.0, - ast.1.iter().map(|fn_def| fn_def.as_ref().clone()).collect(), - ) + let fn_lib = ast.1.iter().map(|fn_def| fn_def.as_ref().clone()).collect(); + optimize_into_ast(self, scope, ast.0, fn_lib) } /// Override default action of `print` (print to stdout using `println!`) @@ -1012,22 +986,24 @@ impl<'e> Engine<'e> { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// # use std::sync::RwLock; /// use rhai::Engine; /// - /// let mut result = String::from(""); + /// let result = RwLock::new(String::from("")); /// { - /// let mut engine = Engine::new(); + /// let mut engine = Engine::new(); /// - /// // Override action of 'print' function - /// engine.on_print(|s| result.push_str(s)); - /// engine.consume("print(40 + 2);")?; + /// // Override action of 'print' function + /// engine.on_print(|s| result.write().unwrap().push_str(s)); + /// + /// engine.consume("print(40 + 2);")?; /// } - /// assert_eq!(result, "42"); + /// assert_eq!(*result.read().unwrap(), "42"); /// # Ok(()) /// # } /// ``` #[cfg(feature = "sync")] - pub fn on_print(&mut self, callback: impl FnMut(&str) + Send + Sync + 'e) { + pub fn on_print(&mut self, callback: impl Fn(&str) + Send + Sync + 'e) { self.on_print = Some(Box::new(callback)); } /// Override default action of `print` (print to stdout using `println!`) @@ -1036,22 +1012,24 @@ impl<'e> Engine<'e> { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// # use std::sync::RwLock; /// use rhai::Engine; /// - /// let mut result = String::from(""); + /// let result = RwLock::new(String::from("")); /// { - /// let mut engine = Engine::new(); + /// let mut engine = Engine::new(); /// - /// // Override action of 'print' function - /// engine.on_print(|s| result.push_str(s)); - /// engine.consume("print(40 + 2);")?; + /// // Override action of 'print' function + /// engine.on_print(|s| result.write().unwrap().push_str(s)); + /// + /// engine.consume("print(40 + 2);")?; /// } - /// assert_eq!(result, "42"); + /// assert_eq!(*result.read().unwrap(), "42"); /// # Ok(()) /// # } /// ``` #[cfg(not(feature = "sync"))] - pub fn on_print(&mut self, callback: impl FnMut(&str) + 'e) { + pub fn on_print(&mut self, callback: impl Fn(&str) + 'e) { self.on_print = Some(Box::new(callback)); } @@ -1061,22 +1039,24 @@ impl<'e> Engine<'e> { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// # use std::sync::RwLock; /// use rhai::Engine; /// - /// let mut result = String::from(""); + /// let result = RwLock::new(String::from("")); /// { - /// let mut engine = Engine::new(); + /// let mut engine = Engine::new(); /// - /// // Override action of 'debug' function - /// engine.on_debug(|s| result.push_str(s)); - /// engine.consume(r#"debug("hello");"#)?; + /// // Override action of 'print' function + /// engine.on_debug(|s| result.write().unwrap().push_str(s)); + /// + /// engine.consume(r#"debug("hello");"#)?; /// } - /// assert_eq!(result, "\"hello\""); + /// assert_eq!(*result.read().unwrap(), r#""hello""#); /// # Ok(()) /// # } /// ``` #[cfg(feature = "sync")] - pub fn on_debug(&mut self, callback: impl FnMut(&str) + Send + Sync + 'e) { + pub fn on_debug(&mut self, callback: impl Fn(&str) + Send + Sync + 'e) { self.on_debug = Some(Box::new(callback)); } /// Override default action of `debug` (print to stdout using `println!`) @@ -1085,22 +1065,24 @@ impl<'e> Engine<'e> { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// # use std::sync::RwLock; /// use rhai::Engine; /// - /// let mut result = String::from(""); + /// let result = RwLock::new(String::from("")); /// { - /// let mut engine = Engine::new(); + /// let mut engine = Engine::new(); /// - /// // Override action of 'debug' function - /// engine.on_debug(|s| result.push_str(s)); - /// engine.consume(r#"debug("hello");"#)?; + /// // Override action of 'print' function + /// engine.on_debug(|s| result.write().unwrap().push_str(s)); + /// + /// engine.consume(r#"debug("hello");"#)?; /// } - /// assert_eq!(result, "\"hello\""); + /// assert_eq!(*result.read().unwrap(), r#""hello""#); /// # Ok(()) /// # } /// ``` #[cfg(not(feature = "sync"))] - pub fn on_debug(&mut self, callback: impl FnMut(&str) + 'e) { + pub fn on_debug(&mut self, callback: impl Fn(&str) + 'e) { self.on_debug = Some(Box::new(callback)); } } diff --git a/src/engine.rs b/src/engine.rs index e8c24034..e0ce5c43 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,7 +1,8 @@ //! Main module defining the script evaluation `Engine`. use crate::any::{Any, AnyExt, Dynamic, Variant}; -use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt, AST, INT}; +use crate::error::ParseErrorType; +use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt, INT}; use crate::result::EvalAltResult; use crate::scope::{EntryRef as ScopeSource, EntryType as ScopeEntryType, Scope}; @@ -232,7 +233,7 @@ impl DerefMut for FunctionsLib { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// -/// let mut engine = Engine::new(); +/// let engine = Engine::new(); /// /// let result = engine.eval::("40 + 2")?; /// @@ -246,13 +247,6 @@ pub struct Engine<'e> { /// A hashmap containing all compiled functions known to the engine. pub(crate) functions: Option, Box>>, - /// A hashmap containing all script-defined functions. - #[cfg(feature = "sync")] - pub(crate) fn_lib: Option>, - /// A hashmap containing all script-defined functions. - #[cfg(not(feature = "sync"))] - pub(crate) fn_lib: Option>, - /// A hashmap containing all iterators known to the engine. pub(crate) type_iterators: Option>>, /// A hashmap mapping type names to pretty-print names. @@ -260,17 +254,17 @@ pub struct Engine<'e> { /// Closure for implementing the `print` command. #[cfg(feature = "sync")] - pub(crate) on_print: Option>, + pub(crate) on_print: Option>, /// Closure for implementing the `print` command. #[cfg(not(feature = "sync"))] - pub(crate) on_print: Option>, + pub(crate) on_print: Option>, /// Closure for implementing the `debug` command. #[cfg(feature = "sync")] - pub(crate) on_debug: Option>, + pub(crate) on_debug: Option>, /// Closure for implementing the `debug` command. #[cfg(not(feature = "sync"))] - pub(crate) on_debug: Option>, + pub(crate) on_debug: Option>, /// Optimize the AST after compilation. #[cfg(not(feature = "no_optimize"))] @@ -299,7 +293,6 @@ impl Default for Engine<'_> { // Create the new scripting Engine let mut engine = Engine { functions: None, - fn_lib: None, type_iterators: None, type_names: Some(type_names), on_print: Some(Box::new(default_print)), // default print/debug implementations @@ -368,7 +361,6 @@ impl Engine<'_> { pub fn new_raw() -> Self { let mut engine = Engine { functions: None, - fn_lib: None, type_iterators: None, type_names: None, on_print: None, @@ -435,8 +427,9 @@ impl Engine<'_> { /// Universal method for calling functions either registered with the `Engine` or written in Rhai pub(crate) fn call_fn_raw( - &mut self, + &self, scope: Option<&mut Scope>, + fn_lib: Option<&FunctionsLib>, fn_name: &str, args: &mut FnCallArgs, def_val: Option<&Dynamic>, @@ -444,8 +437,8 @@ impl Engine<'_> { level: usize, ) -> Result { // First search in script-defined functions (can override built-in) - if let Some(fn_lib_arc) = &self.fn_lib { - if let Some(fn_def) = fn_lib_arc.clone().get_function(fn_name, args.len()) { + if let Some(lib) = fn_lib { + if let Some(fn_def) = lib.get_function(fn_name, args.len()) { match scope { // Extern scope passed in which is not empty Some(scope) if scope.len() > 0 => { @@ -462,13 +455,13 @@ impl Engine<'_> { ); // Evaluate the function at one higher level of call depth - let result = self.eval_stmt(scope, &fn_def.body, level + 1).or_else( - |err| match err { + let result = self + .eval_stmt(scope, fn_lib, &fn_def.body, level + 1) + .or_else(|err| match err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), err => Err(err.set_position(pos)), - }, - ); + }); scope.rewind(scope_len); @@ -488,13 +481,13 @@ impl Engine<'_> { ); // Evaluate the function at one higher level of call depth - return self.eval_stmt(&mut scope, &fn_def.body, level + 1).or_else( - |err| match err { + return self + .eval_stmt(&mut scope, fn_lib, &fn_def.body, level + 1) + .or_else(|err| match err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), err => Err(err.set_position(pos)), - }, - ); + }); } } } @@ -521,11 +514,11 @@ impl Engine<'_> { // See if the function match print/debug (which requires special processing) return Ok(match fn_name { KEYWORD_PRINT if self.on_print.is_some() => { - self.on_print.as_deref_mut().unwrap()(cast_to_string(result.as_ref(), pos)?) + self.on_print.as_ref().unwrap()(cast_to_string(result.as_ref(), pos)?) .into_dynamic() } KEYWORD_DEBUG if self.on_debug.is_some() => { - self.on_debug.as_deref_mut().unwrap()(cast_to_string(result.as_ref(), pos)?) + self.on_debug.as_ref().unwrap()(cast_to_string(result.as_ref(), pos)?) .into_dynamic() } KEYWORD_PRINT | KEYWORD_DEBUG => ().into_dynamic(), @@ -590,8 +583,9 @@ impl Engine<'_> { /// Chain-evaluate a dot setter. #[cfg(not(feature = "no_object"))] fn get_dot_val_helper( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, target: Target, dot_rhs: &Expr, level: usize, @@ -601,7 +595,7 @@ impl Engine<'_> { Expr::FunctionCall(fn_name, arg_expr_list, def_val, pos) => { let mut values = arg_expr_list .iter() - .map(|arg_expr| self.eval_expr(scope, arg_expr, level)) + .map(|arg_expr| self.eval_expr(scope, fn_lib, arg_expr, level)) .collect::, _>>()?; let this_ptr = target.get_mut(scope); @@ -612,13 +606,13 @@ impl Engine<'_> { let def_val = def_val.as_ref(); - self.call_fn_raw(None, fn_name, &mut args, def_val, *pos, 0) + self.call_fn_raw(None, fn_lib, fn_name, &mut args, def_val, *pos, 0) } // xxx.id Expr::Property(id, pos) => { let mut args = [target.get_mut(scope)]; - self.call_fn_raw(None, &make_getter(id), &mut args, None, *pos, 0) + self.call_fn_raw(None, fn_lib, &make_getter(id), &mut args, None, *pos, 0) } // xxx.idx_lhs[idx_expr] @@ -628,11 +622,11 @@ impl Engine<'_> { // xxx.id[idx_expr] Expr::Property(id, pos) => { let mut args = [target.get_mut(scope)]; - self.call_fn_raw(None, &make_getter(id), &mut args, None, *pos, 0)? + self.call_fn_raw(None, fn_lib, &make_getter(id), &mut args, None, *pos, 0)? } // xxx.???[???][idx_expr] Expr::Index(_, _, _) => { - self.get_dot_val_helper(scope, target, idx_lhs, level)? + self.get_dot_val_helper(scope, fn_lib, target, idx_lhs, level)? } // Syntax error _ => { @@ -643,7 +637,7 @@ impl Engine<'_> { } }; - self.get_indexed_value(scope, &value, idx_expr, *op_pos, level) + self.get_indexed_value(scope, fn_lib, &value, idx_expr, *op_pos, level) .map(|(val, _, _)| val) } @@ -652,9 +646,10 @@ impl Engine<'_> { // xxx.id.rhs Expr::Property(id, pos) => { let mut args = [target.get_mut(scope)]; - self.call_fn_raw(None, &make_getter(id), &mut args, None, *pos, 0) + self.call_fn_raw(None, fn_lib, &make_getter(id), &mut args, None, *pos, 0) .and_then(|mut val| { - self.get_dot_val_helper(scope, Target::from(val.as_mut()), rhs, level) + let target = Target::from(val.as_mut()); + self.get_dot_val_helper(scope, fn_lib, target, rhs, level) }) } // xxx.idx_lhs[idx_expr].rhs @@ -663,12 +658,13 @@ impl Engine<'_> { let val = match idx_lhs.as_ref() { // xxx.id[idx_expr].rhs Expr::Property(id, pos) => { + let fn_name = make_getter(id); let mut args = [target.get_mut(scope)]; - self.call_fn_raw(None, &make_getter(id), &mut args, None, *pos, 0)? + self.call_fn_raw(None, fn_lib, &fn_name, &mut args, None, *pos, 0)? } // xxx.???[???][idx_expr].rhs Expr::Index(_, _, _) => { - self.get_dot_val_helper(scope, target, idx_lhs, level)? + self.get_dot_val_helper(scope, fn_lib, target, idx_lhs, level)? } // Syntax error _ => { @@ -679,9 +675,10 @@ impl Engine<'_> { } }; - self.get_indexed_value(scope, &val, idx_expr, *op_pos, level) + self.get_indexed_value(scope, fn_lib, &val, idx_expr, *op_pos, level) .and_then(|(mut val, _, _)| { - self.get_dot_val_helper(scope, Target::from(val.as_mut()), rhs, level) + let target = Target::from(val.as_mut()); + self.get_dot_val_helper(scope, fn_lib, target, rhs, level) }) } // Syntax error @@ -702,8 +699,9 @@ impl Engine<'_> { /// Evaluate a dot chain getter #[cfg(not(feature = "no_object"))] fn get_dot_val( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, dot_lhs: &Expr, dot_rhs: &Expr, level: usize, @@ -718,16 +716,16 @@ impl Engine<'_> { // This is a variable property access (potential function call). // Use a direct index into `scope` to directly mutate the variable value. - self.get_dot_val_helper(scope, Target::from_src(entry), dot_rhs, level) + self.get_dot_val_helper(scope, fn_lib, Target::from_src(entry), dot_rhs, level) } // idx_lhs[idx_expr].??? #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, idx_expr, op_pos) => { let (idx_src_type, src, idx, mut val) = - self.eval_index_expr(scope, idx_lhs, idx_expr, *op_pos, level)?; - let value = - self.get_dot_val_helper(scope, Target::from(val.as_mut()), dot_rhs, level); + self.eval_index_expr(scope, fn_lib, idx_lhs, idx_expr, *op_pos, level)?; + let target = Target::from(val.as_mut()); + let value = self.get_dot_val_helper(scope, fn_lib, target, dot_rhs, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some(src) = src { @@ -755,8 +753,8 @@ impl Engine<'_> { // {expr}.??? expr => { - let mut val = self.eval_expr(scope, expr, level)?; - self.get_dot_val_helper(scope, Target::from(val.as_mut()), dot_rhs, level) + let mut val = self.eval_expr(scope, fn_lib, expr, level)?; + self.get_dot_val_helper(scope, fn_lib, Target::from(val.as_mut()), dot_rhs, level) } } } @@ -775,8 +773,9 @@ impl Engine<'_> { /// Get the value at the indexed position of a base type #[cfg(not(feature = "no_index"))] fn get_indexed_value( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, val: &Dynamic, idx_expr: &Expr, op_pos: Position, @@ -787,7 +786,7 @@ impl Engine<'_> { // val_array[idx] if let Some(arr) = val.downcast_ref::() { let idx = self - .eval_expr(scope, idx_expr, level)? + .eval_expr(scope, fn_lib, idx_expr, level)? .try_cast::() .map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?; @@ -806,7 +805,7 @@ impl Engine<'_> { // val_map[idx] if let Some(map) = val.downcast_ref::() { let idx = self - .eval_expr(scope, idx_expr, level)? + .eval_expr(scope, fn_lib, idx_expr, level)? .try_cast::() .map_err(|_| EvalAltResult::ErrorStringIndexExpr(idx_expr.position()))?; @@ -821,7 +820,7 @@ impl Engine<'_> { // val_string[idx] if let Some(s) = val.downcast_ref::() { let idx = self - .eval_expr(scope, idx_expr, level)? + .eval_expr(scope, fn_lib, idx_expr, level)? .try_cast::() .map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?; @@ -857,8 +856,9 @@ impl Engine<'_> { /// Evaluate an index expression #[cfg(not(feature = "no_index"))] fn eval_index_expr<'a>( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, lhs: &'a Expr, idx_expr: &Expr, op_pos: Position, @@ -885,7 +885,7 @@ impl Engine<'_> { ) = Self::search_scope(scope, &id, lhs.position())?; let (val, idx_src_type, idx) = - self.get_indexed_value(scope, &val, idx_expr, op_pos, level)?; + self.get_indexed_value(scope, fn_lib, &val, idx_expr, op_pos, level)?; Ok(( idx_src_type, @@ -901,9 +901,9 @@ impl Engine<'_> { // (expr)[idx_expr] expr => { - let val = self.eval_expr(scope, expr, level)?; + let val = self.eval_expr(scope, fn_lib, expr, level)?; - self.get_indexed_value(scope, &val, idx_expr, op_pos, level) + self.get_indexed_value(scope, fn_lib, &val, idx_expr, op_pos, level) .map(|(val, _, idx)| (IndexSourceType::Expression, None, idx, val)) } } @@ -1002,8 +1002,9 @@ impl Engine<'_> { /// Chain-evaluate a dot setter #[cfg(not(feature = "no_object"))] fn set_dot_val_helper( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, this_ptr: &mut Variant, dot_rhs: &Expr, new_val: (&mut Dynamic, Position), @@ -1013,7 +1014,7 @@ impl Engine<'_> { // xxx.id Expr::Property(id, pos) => { let mut args = [this_ptr, new_val.0.as_mut()]; - self.call_fn_raw(None, &make_setter(id), &mut args, None, *pos, 0) + self.call_fn_raw(None, fn_lib, &make_setter(id), &mut args, None, *pos, 0) } // xxx.lhs[idx_expr] @@ -1021,18 +1022,21 @@ impl Engine<'_> { #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() { // xxx.id[idx_expr] - Expr::Property(id, pos) => self - .call_fn_raw(None, &make_getter(id), &mut [this_ptr], None, *pos, 0) - .and_then(|val| { - let (_, _, idx) = - self.get_indexed_value(scope, &val, idx_expr, *op_pos, level)?; + Expr::Property(id, pos) => { + let fn_name = make_getter(id); + self.call_fn_raw(None, fn_lib, &fn_name, &mut [this_ptr], None, *pos, 0) + .and_then(|val| { + let (_, _, idx) = self + .get_indexed_value(scope, fn_lib, &val, idx_expr, *op_pos, level)?; - Self::update_indexed_value(val, idx, new_val.0.clone(), new_val.1) - }) - .and_then(|mut val| { - let mut args = [this_ptr, val.as_mut()]; - self.call_fn_raw(None, &make_setter(id), &mut args, None, *pos, 0) - }), + Self::update_indexed_value(val, idx, new_val.0.clone(), new_val.1) + }) + .and_then(|mut val| { + let fn_name = make_setter(id); + let mut args = [this_ptr, val.as_mut()]; + self.call_fn_raw(None, fn_lib, &fn_name, &mut args, None, *pos, 0) + }) + } // All others - syntax error for setters chain _ => Err(EvalAltResult::ErrorDotExpr( @@ -1045,14 +1049,17 @@ impl Engine<'_> { Expr::Dot(lhs, rhs, _) => match lhs.as_ref() { // xxx.id.rhs Expr::Property(id, pos) => { - self.call_fn_raw(None, &make_getter(id), &mut [this_ptr], None, *pos, 0) + let fn_name = make_getter(id); + self.call_fn_raw(None, fn_lib, &fn_name, &mut [this_ptr], None, *pos, 0) .and_then(|mut val| { - self.set_dot_val_helper(scope, val.as_mut(), rhs, new_val, level) + let value = val.as_mut(); + self.set_dot_val_helper(scope, fn_lib, value, rhs, new_val, level) .map(|_| val) // Discard Ok return value }) .and_then(|mut val| { + let fn_name = make_setter(id); let mut args = [this_ptr, val.as_mut()]; - self.call_fn_raw(None, &make_setter(id), &mut args, None, *pos, 0) + self.call_fn_raw(None, fn_lib, &fn_name, &mut args, None, *pos, 0) }) } @@ -1062,21 +1069,26 @@ impl Engine<'_> { Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() { // xxx.id[idx_expr].rhs Expr::Property(id, pos) => { - self.call_fn_raw(None, &make_getter(id), &mut [this_ptr], None, *pos, 0) + let fn_name = make_getter(id); + self.call_fn_raw(None, fn_lib, &fn_name, &mut [this_ptr], None, *pos, 0) .and_then(|v| { - let (mut value, _, idx) = - self.get_indexed_value(scope, &v, idx_expr, *op_pos, level)?; + let (mut value, _, idx) = self.get_indexed_value( + scope, fn_lib, &v, idx_expr, *op_pos, level, + )?; let val_pos = new_val.1; let this_ptr = value.as_mut(); - self.set_dot_val_helper(scope, this_ptr, rhs, new_val, level)?; + self.set_dot_val_helper( + scope, fn_lib, this_ptr, rhs, new_val, level, + )?; // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. Self::update_indexed_value(v, idx, value, val_pos) }) .and_then(|mut v| { + let fn_name = make_setter(id); let mut args = [this_ptr, v.as_mut()]; - self.call_fn_raw(None, &make_setter(id), &mut args, None, *pos, 0) + self.call_fn_raw(None, fn_lib, &fn_name, &mut args, None, *pos, 0) }) } @@ -1105,8 +1117,9 @@ impl Engine<'_> { // Evaluate a dot chain setter #[cfg(not(feature = "no_object"))] fn set_dot_val( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, dot_lhs: &Expr, dot_rhs: &Expr, new_val: (&mut Dynamic, Position), @@ -1127,8 +1140,8 @@ impl Engine<'_> { // Avoid referencing scope which is used below as mut let entry = ScopeSource { name: id, ..entry }; let this_ptr = target.as_mut(); - let value = - self.set_dot_val_helper(scope, this_ptr, dot_rhs, new_val, level); + let value = self + .set_dot_val_helper(scope, fn_lib, this_ptr, dot_rhs, new_val, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. *scope.get_mut(entry) = target; @@ -1143,10 +1156,11 @@ impl Engine<'_> { #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => { let (idx_src_type, src, idx, mut target) = - self.eval_index_expr(scope, lhs, idx_expr, *op_pos, level)?; + self.eval_index_expr(scope, fn_lib, lhs, idx_expr, *op_pos, level)?; let val_pos = new_val.1; let this_ptr = target.as_mut(); - let value = self.set_dot_val_helper(scope, this_ptr, dot_rhs, new_val, level); + let value = + self.set_dot_val_helper(scope, fn_lib, this_ptr, dot_rhs, new_val, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. if let Some(src) = src { @@ -1182,14 +1196,15 @@ impl Engine<'_> { // Evaluate an 'in' expression fn eval_in_expr( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, lhs: &Expr, rhs: &Expr, level: usize, ) -> Result { - let mut lhs_value = self.eval_expr(scope, lhs, level)?; - let rhs_value = self.eval_expr(scope, rhs, level)?; + let mut lhs_value = self.eval_expr(scope, fn_lib, lhs, level)?; + let rhs_value = self.eval_expr(scope, fn_lib, rhs, level)?; #[cfg(not(feature = "no_index"))] { @@ -1200,15 +1215,10 @@ impl Engine<'_> { // Call the '==' operator to compare each value for value in rhs_value.iter_mut() { + let args = &mut [lhs_value.as_mut(), value.as_mut()]; + let def_value = Some(&def_value); if self - .call_fn_raw( - None, - "==", - &mut [lhs_value.as_mut(), value.as_mut()], - Some(&def_value), - rhs.position(), - level, - )? + .call_fn_raw(None, fn_lib, "==", args, def_value, rhs.position(), level)? .try_cast::() .unwrap_or(false) { @@ -1261,8 +1271,9 @@ impl Engine<'_> { /// Evaluate an expression fn eval_expr( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, expr: &Expr, level: usize, ) -> Result { @@ -1279,15 +1290,15 @@ impl Engine<'_> { // lhs[idx_expr] #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => self - .eval_index_expr(scope, lhs, idx_expr, *op_pos, level) + .eval_index_expr(scope, fn_lib, lhs, idx_expr, *op_pos, level) .map(|(_, _, _, x)| x), // Statement block - Expr::Stmt(stmt, _) => self.eval_stmt(scope, stmt, level), + Expr::Stmt(stmt, _) => self.eval_stmt(scope, fn_lib, stmt, level), // lhs = rhs Expr::Assignment(lhs, rhs, op_pos) => { - let mut rhs_val = self.eval_expr(scope, rhs, level)?; + let mut rhs_val = self.eval_expr(scope, fn_lib, rhs, level)?; match lhs.as_ref() { // name = rhs @@ -1324,7 +1335,7 @@ impl Engine<'_> { #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, idx_expr, op_pos) => { let (idx_src_type, src, idx, _) = - self.eval_index_expr(scope, idx_lhs, idx_expr, *op_pos, level)?; + self.eval_index_expr(scope, fn_lib, idx_lhs, idx_expr, *op_pos, level)?; if let Some(src) = src { match src.typ { @@ -1351,14 +1362,10 @@ impl Engine<'_> { // dot_lhs.dot_rhs = rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(dot_lhs, dot_rhs, _) => self.set_dot_val( - scope, - dot_lhs, - dot_rhs, - (&mut rhs_val, rhs.position()), - *op_pos, - level, - ), + Expr::Dot(dot_lhs, dot_rhs, _) => { + let new_val = (&mut rhs_val, rhs.position()); + self.set_dot_val(scope, fn_lib, dot_lhs, dot_rhs, new_val, *op_pos, level) + } // Error assignment to constant expr if expr.is_constant() => Err(EvalAltResult::ErrorAssignmentToConstant( @@ -1372,14 +1379,15 @@ impl Engine<'_> { } #[cfg(not(feature = "no_object"))] - Expr::Dot(lhs, rhs, _) => self.get_dot_val(scope, lhs, rhs, level), + Expr::Dot(lhs, rhs, _) => self.get_dot_val(scope, fn_lib, lhs, rhs, level), #[cfg(not(feature = "no_index"))] Expr::Array(contents, _) => { let mut arr = Array::new(); contents.into_iter().try_for_each(|item| { - self.eval_expr(scope, item, level).map(|val| arr.push(val)) + self.eval_expr(scope, fn_lib, item, level) + .map(|val| arr.push(val)) })?; Ok((arr).into_dynamic()) @@ -1390,7 +1398,7 @@ impl Engine<'_> { let mut map = Map::new(); contents.into_iter().try_for_each(|item| { - self.eval_expr(scope, &item.1, level).map(|val| { + self.eval_expr(scope, fn_lib, &item.1, level).map(|val| { map.insert(item.0.clone(), val); }) })?; @@ -1400,14 +1408,17 @@ impl Engine<'_> { Expr::FunctionCall(fn_name, args_expr_list, def_val, pos) => { // Has a system function an override? - fn has_override(engine: &Engine, name: &str) -> bool { - (engine.functions.is_some() && { - engine.functions.as_ref().unwrap().contains_key(&FnSpec { + fn has_override( + engine: &Engine, + fn_lib: Option<&FunctionsLib>, + name: &str, + ) -> bool { + engine.functions.as_ref().map_or(false, |lib| { + lib.contains_key(&FnSpec { name: name.into(), args: vec![TypeId::of::()], }) - }) || (engine.fn_lib.is_some() - && engine.fn_lib.as_ref().unwrap().has_function(name, 1)) + }) || fn_lib.map_or(false, |lib| lib.has_function(name, 1)) } match fn_name.as_ref() { @@ -1429,14 +1440,15 @@ impl Engine<'_> { // Redirect call to `print` let mut args = [result.as_mut()]; - self.call_fn_raw(None, KEYWORD_PRINT, &mut args, None, pos, level) + self.call_fn_raw(None, fn_lib, KEYWORD_PRINT, &mut args, None, pos, level) } // type_of KEYWORD_TYPE_OF - if args_expr_list.len() == 1 && !has_override(self, KEYWORD_TYPE_OF) => + if args_expr_list.len() == 1 + && !has_override(self, fn_lib, KEYWORD_TYPE_OF) => { - let r = self.eval_expr(scope, &args_expr_list[0], level)?; + let r = self.eval_expr(scope, fn_lib, &args_expr_list[0], level)?; Ok(self .map_type_name((*r).type_name()) .to_string() @@ -1445,10 +1457,11 @@ impl Engine<'_> { // eval KEYWORD_EVAL - if args_expr_list.len() == 1 && !has_override(self, KEYWORD_EVAL) => + if args_expr_list.len() == 1 + && !has_override(self, fn_lib, KEYWORD_EVAL) => { let pos = args_expr_list[0].position(); - let r = self.eval_expr(scope, &args_expr_list[0], level)?; + let r = self.eval_expr(scope, fn_lib, &args_expr_list[0], level)?; // Get the script text by evaluating the expression let script = @@ -1462,81 +1475,59 @@ impl Engine<'_> { })?; // Compile the script text - #[cfg(not(feature = "no_optimize"))] - let ast = { - let orig_optimization_level = self.optimization_level; + let mut ast = self.compile(script).map_err(EvalAltResult::ErrorParsing)?; - self.set_optimization_level(OptimizationLevel::None); - let ast = self.compile(script); - self.set_optimization_level(orig_optimization_level); + // If new functions are defined within the eval string, it is an error + if ast.1.len() > 0 { + return Err(EvalAltResult::ErrorParsing( + ParseErrorType::WrongFnDefinition.into_err(pos), + )); + } - ast.map_err(EvalAltResult::ErrorParsing)? - }; - - #[cfg(feature = "no_optimize")] - let ast = self.compile(script).map_err(EvalAltResult::ErrorParsing)?; - - // If new functions are defined, merge it into the current functions library - let merged = AST( - ast.0, - if let Some(fn_lib) = &self.fn_lib { - #[cfg(feature = "sync")] - { - Arc::new(fn_lib.as_ref().merge(&ast.1)) - } - #[cfg(not(feature = "sync"))] - { - Rc::new(fn_lib.as_ref().merge(&ast.1)) - } - } else { - ast.1 - }, - ); + if let Some(lib) = fn_lib { + #[cfg(feature = "sync")] + { + ast.1 = Arc::new(lib.clone()); + } + #[cfg(not(feature = "sync"))] + { + ast.1 = Rc::new(lib.clone()); + } + } // Evaluate the AST - let result = self - .eval_ast_with_scope_raw(scope, &merged) - .map_err(|err| err.set_position(pos)); - - // Update the new functions library if there are new functions - self.fn_lib = if !merged.1.is_empty() { - Some(merged.1) - } else { - None - }; - - Ok(result?) + self.eval_ast_with_scope_raw(scope, &ast) + .map_err(|err| err.set_position(pos)) } // Normal function call _ => { - let mut values = args_expr_list + let mut arg_values = args_expr_list .iter() - .map(|expr| self.eval_expr(scope, expr, level)) + .map(|expr| self.eval_expr(scope, fn_lib, expr, level)) .collect::, _>>()?; - let mut arg_values: Vec<_> = - values.iter_mut().map(Dynamic::as_mut).collect(); - + let mut args: Vec<_> = arg_values.iter_mut().map(Dynamic::as_mut).collect(); let def_val = def_val.as_ref(); - - self.call_fn_raw(None, fn_name, &mut arg_values, def_val, *pos, level) + self.call_fn_raw(None, fn_lib, fn_name, &mut args, def_val, *pos, level) } } } - Expr::In(lhs, rhs, _) => self.eval_in_expr(scope, lhs.as_ref(), rhs.as_ref(), level), + Expr::In(lhs, rhs, _) => { + self.eval_in_expr(scope, fn_lib, lhs.as_ref(), rhs.as_ref(), level) + } Expr::And(lhs, rhs, _) => Ok(Box::new( self - .eval_expr(scope, lhs.as_ref(), level)? + .eval_expr(scope, fn_lib,lhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), lhs.position()) })? && // Short-circuit using && self - .eval_expr(scope, rhs.as_ref(), level)? + .eval_expr(scope, fn_lib,rhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) @@ -1545,14 +1536,14 @@ impl Engine<'_> { Expr::Or(lhs, rhs, _) => Ok(Box::new( self - .eval_expr(scope, lhs.as_ref(), level)? + .eval_expr(scope,fn_lib, lhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), lhs.position()) })? || // Short-circuit using || self - .eval_expr(scope, rhs.as_ref(), level)? + .eval_expr(scope,fn_lib, rhs.as_ref(), level)? .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) @@ -1567,8 +1558,9 @@ impl Engine<'_> { /// Evaluate a statement pub(crate) fn eval_stmt( - &mut self, + &self, scope: &mut Scope, + fn_lib: Option<&FunctionsLib>, stmt: &Stmt, level: usize, ) -> Result { @@ -1578,7 +1570,7 @@ impl Engine<'_> { // Expression as statement Stmt::Expr(expr) => { - let result = self.eval_expr(scope, expr, level)?; + let result = self.eval_expr(scope, fn_lib, expr, level)?; Ok(if !matches!(expr.as_ref(), Expr::Assignment(_, _, _)) { result @@ -1593,7 +1585,7 @@ impl Engine<'_> { let prev_len = scope.len(); let result = block.iter().try_fold(().into_dynamic(), |_, stmt| { - self.eval_stmt(scope, stmt, level) + self.eval_stmt(scope, fn_lib, stmt, level) }); scope.rewind(prev_len); @@ -1603,14 +1595,14 @@ impl Engine<'_> { // If-else statement Stmt::IfThenElse(guard, if_body, else_body) => self - .eval_expr(scope, guard, level)? + .eval_expr(scope, fn_lib, guard, level)? .try_cast::() .map_err(|_| EvalAltResult::ErrorLogicGuard(guard.position())) .and_then(|guard_val| { if guard_val { - self.eval_stmt(scope, if_body, level) + self.eval_stmt(scope, fn_lib, if_body, level) } else if let Some(stmt) = else_body { - self.eval_stmt(scope, stmt.as_ref(), level) + self.eval_stmt(scope, fn_lib, stmt.as_ref(), level) } else { Ok(().into_dynamic()) } @@ -1618,12 +1610,19 @@ impl Engine<'_> { // While loop Stmt::While(guard, body) => loop { - match self.eval_expr(scope, guard, level)?.try_cast::() { - Ok(guard_val) if guard_val => match self.eval_stmt(scope, body, level) { - Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), - Err(EvalAltResult::ErrorLoopBreak(true, _)) => return Ok(().into_dynamic()), - Err(x) => return Err(x), - }, + match self + .eval_expr(scope, fn_lib, guard, level)? + .try_cast::() + { + Ok(guard_val) if guard_val => { + match self.eval_stmt(scope, fn_lib, body, level) { + Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), + Err(EvalAltResult::ErrorLoopBreak(true, _)) => { + return Ok(().into_dynamic()) + } + Err(x) => return Err(x), + } + } Ok(_) => return Ok(().into_dynamic()), Err(_) => return Err(EvalAltResult::ErrorLogicGuard(guard.position())), } @@ -1631,7 +1630,7 @@ impl Engine<'_> { // Loop statement Stmt::Loop(body) => loop { - match self.eval_stmt(scope, body, level) { + match self.eval_stmt(scope, fn_lib, body, level) { Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), Err(EvalAltResult::ErrorLoopBreak(true, _)) => return Ok(().into_dynamic()), Err(x) => return Err(x), @@ -1640,7 +1639,7 @@ impl Engine<'_> { // For loop Stmt::For(name, expr, body) => { - let arr = self.eval_expr(scope, expr, level)?; + let arr = self.eval_expr(scope, fn_lib, expr, level)?; let tid = Any::type_id(arr.as_ref()); if let Some(type_iterators) = &self.type_iterators { @@ -1658,7 +1657,7 @@ impl Engine<'_> { for a in iter_fn(&arr) { *scope.get_mut(entry) = a; - match self.eval_stmt(scope, body, level) { + match self.eval_stmt(scope, fn_lib, body, level) { Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), Err(EvalAltResult::ErrorLoopBreak(true, _)) => break, Err(x) => return Err(x), @@ -1688,7 +1687,7 @@ impl Engine<'_> { // Return value Stmt::ReturnWithVal(Some(a), ReturnType::Return, pos) => Err(EvalAltResult::Return( - self.eval_expr(scope, a, level)?, + self.eval_expr(scope, fn_lib, a, level)?, *pos, )), @@ -1699,7 +1698,7 @@ impl Engine<'_> { // Throw value Stmt::ReturnWithVal(Some(a), ReturnType::Exception, pos) => { - let val = self.eval_expr(scope, a, level)?; + let val = self.eval_expr(scope, fn_lib, a, level)?; Err(EvalAltResult::ErrorRuntime( val.try_cast::().unwrap_or_else(|_| "".to_string()), *pos, @@ -1708,7 +1707,7 @@ impl Engine<'_> { // Let statement Stmt::Let(name, Some(expr), _) => { - let val = self.eval_expr(scope, expr, level)?; + let val = self.eval_expr(scope, fn_lib, expr, level)?; // TODO - avoid copying variable name in inner block? scope.push_dynamic_value(name.clone(), ScopeEntryType::Normal, val, false); Ok(().into_dynamic()) @@ -1722,7 +1721,7 @@ impl Engine<'_> { // Const statement Stmt::Const(name, expr, _) if expr.is_constant() => { - let val = self.eval_expr(scope, expr, level)?; + let val = self.eval_expr(scope, fn_lib, expr, level)?; // TODO - avoid copying variable name in inner block? scope.push_dynamic_value(name.clone(), ScopeEntryType::Constant, val, true); Ok(().into_dynamic()) @@ -1745,11 +1744,6 @@ impl Engine<'_> { .unwrap_or(name) } } - - /// Clean up all script-defined functions within the `Engine`. - pub fn clear_functions(&mut self) { - self.fn_lib = None; - } } /// Print/debug to stdout diff --git a/src/lib.rs b/src/lib.rs index 3321fccc..dba21d7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ //! //! engine.register_fn("compute_something", compute_something); //! -//! # #[cfg(not(feature = "no_std"))] +//! # #[cfg(not(feature = "no_std"))] //! assert_eq!(engine.eval_file::("my_script.rhai".into())?, true); //! //! Ok(()) diff --git a/src/optimize.rs b/src/optimize.rs index f20e719a..85c61d1e 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -39,15 +39,18 @@ struct State<'a> { constants: Vec<(String, Expr)>, /// An `Engine` instance for eager function evaluation. engine: &'a Engine<'a>, + /// Library of script-defined functions. + fn_lib: &'a [(&'a str, usize)], } impl<'a> State<'a> { /// Create a new State. - pub fn new(engine: &'a Engine<'a>) -> Self { + pub fn new(engine: &'a Engine<'a>, fn_lib: &'a [(&'a str, usize)]) -> Self { Self { changed: false, constants: vec![], engine, + fn_lib, } } /// Reset the state from dirty to clean. @@ -502,11 +505,9 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { && args.iter().all(|expr| expr.is_constant()) // all arguments are constants => { // First search in script-defined functions (can override built-in) - if let Some(fn_lib_arc) = &state.engine.fn_lib { - if fn_lib_arc.has_function(&id, args.len()) { - // A script-defined function overrides the built-in function - do not make the call - return Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos); - } + if state.fn_lib.iter().find(|(name, len)| name == &id && *len == args.len()).is_some() { + // A script-defined function overrides the built-in function - do not make the call + return Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos); } let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); @@ -554,14 +555,19 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { } } -pub(crate) fn optimize<'a>(statements: Vec, engine: &Engine<'a>, scope: &Scope) -> Vec { +pub(crate) fn optimize<'a>( + statements: Vec, + engine: &Engine<'a>, + scope: &Scope, + fn_lib: &'a [(&'a str, usize)], +) -> Vec { // If optimization level is None then skip optimizing if engine.optimization_level == OptimizationLevel::None { return statements; } // Set up the state - let mut state = State::new(engine); + let mut state = State::new(engine, fn_lib); // Add constants from the scope into the state scope @@ -635,7 +641,12 @@ pub fn optimize_into_ast( statements: Vec, functions: Vec, ) -> AST { - let fn_lib = FunctionsLib::from_vec( + let fn_lib: Vec<_> = functions + .iter() + .map(|fn_def| (fn_def.name.as_str(), fn_def.params.len())) + .collect(); + + let lib = FunctionsLib::from_vec( functions .iter() .cloned() @@ -644,7 +655,7 @@ pub fn optimize_into_ast( let pos = fn_def.body.position(); // Optimize the function body - let mut body = optimize(vec![fn_def.body], engine, &Scope::new()); + let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &fn_lib); // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { @@ -667,12 +678,12 @@ pub fn optimize_into_ast( match engine.optimization_level { OptimizationLevel::None => statements, OptimizationLevel::Simple | OptimizationLevel::Full => { - optimize(statements, engine, &scope) + optimize(statements, engine, &scope, &fn_lib) } }, #[cfg(feature = "sync")] - Arc::new(fn_lib), + Arc::new(lib), #[cfg(not(feature = "sync"))] - Rc::new(fn_lib), + Rc::new(lib), ) } diff --git a/src/parser.rs b/src/parser.rs index e425dc4b..98a3aff3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -199,7 +199,7 @@ impl AST { /// # { /// use rhai::Engine; /// - /// let mut engine = Engine::new(); + /// let engine = Engine::new(); /// /// let ast1 = engine.compile(r#"fn foo(x) { 42 + x } foo(1)"#)?; /// let ast2 = engine.compile(r#"fn foo(n) { "hello" + n } foo("!")"#)?; diff --git a/src/result.rs b/src/result.rs index b9f57f86..29af3bbd 100644 --- a/src/result.rs +++ b/src/result.rs @@ -16,6 +16,8 @@ use crate::stdlib::path::PathBuf; /// Evaluation result. /// /// All wrapped `Position` values represent the location in the script where the error occurs. +/// +/// Currently, `EvalAltResult` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug)] pub enum EvalAltResult { /// Syntax error. diff --git a/src/scope.rs b/src/scope.rs index 15eda16c..b5e22170 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -49,7 +49,7 @@ pub(crate) struct EntryRef<'a> { /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// -/// let mut engine = Engine::new(); +/// let engine = Engine::new(); /// let mut my_scope = Scope::new(); /// /// my_scope.push("z", 40_i64); diff --git a/tests/arrays.rs b/tests/arrays.rs index 438ae4ea..e1dc066a 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -3,7 +3,7 @@ use rhai::{Array, Engine, EvalAltResult, RegisterFn, INT}; #[test] fn test_arrays() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); 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); diff --git a/tests/binary_ops.rs b/tests/binary_ops.rs index 3e59e8fd..922c6a43 100644 --- a/tests/binary_ops.rs +++ b/tests/binary_ops.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_binary_ops() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("10 % 4")?, 2); assert_eq!(engine.eval::("10 << 4")?, 160); diff --git a/tests/bit_shift.rs b/tests/bit_shift.rs index 7dd3aa76..e8e360f3 100644 --- a/tests/bit_shift.rs +++ b/tests/bit_shift.rs @@ -2,14 +2,14 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_left_shift() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("4 << 2")?, 16); Ok(()) } #[test] fn test_right_shift() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("9 >> 1")?, 4); Ok(()) } diff --git a/tests/bool_op.rs b/tests/bool_op.rs index a39001a2..3bd5859a 100644 --- a/tests/bool_op.rs +++ b/tests/bool_op.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_bool_op1() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("true && (false || true)")?, true); assert_eq!(engine.eval::("true & (false | true)")?, true); @@ -12,7 +12,7 @@ fn test_bool_op1() -> Result<(), EvalAltResult> { #[test] fn test_bool_op2() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("false && (false || true)")?, false); assert_eq!(engine.eval::("false & (false | true)")?, false); @@ -22,7 +22,7 @@ fn test_bool_op2() -> Result<(), EvalAltResult> { #[test] fn test_bool_op3() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert!(engine.eval::("true && (false || 123)").is_err()); assert_eq!(engine.eval::("true && (true || 123)")?, true); @@ -34,7 +34,7 @@ fn test_bool_op3() -> Result<(), EvalAltResult> { #[test] fn test_bool_op_short_circuit() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( @@ -63,7 +63,7 @@ fn test_bool_op_short_circuit() -> Result<(), EvalAltResult> { #[test] fn test_bool_op_no_short_circuit1() { - let mut engine = Engine::new(); + let engine = Engine::new(); assert!(engine .eval::( @@ -78,7 +78,7 @@ fn test_bool_op_no_short_circuit1() { #[test] fn test_bool_op_no_short_circuit2() { - let mut engine = Engine::new(); + let engine = Engine::new(); assert!(engine .eval::( diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 909e8a90..b0b3793d 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -20,7 +20,7 @@ fn test_fn() -> Result<(), EvalAltResult> { #[test] fn test_call_fn() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let mut scope = Scope::new(); scope.push("foo", 42 as INT); diff --git a/tests/chars.rs b/tests/chars.rs index 2739401b..8d6be4d8 100644 --- a/tests/chars.rs +++ b/tests/chars.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_chars() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("'y'")?, 'y'); assert_eq!(engine.eval::("'\\u2764'")?, '❤'); diff --git a/tests/comments.rs b/tests/comments.rs index 8c071f07..d49b7586 100644 --- a/tests/comments.rs +++ b/tests/comments.rs @@ -2,13 +2,13 @@ use rhai::{Engine, INT}; #[test] fn test_comments() { - let mut engine = Engine::new(); + let engine = Engine::new(); assert!(engine .eval::("let x = 5; x // I am a single line comment, yay!") .is_ok()); assert!(engine - .eval::("let /* I am a multiline comment, yay! */ x = 5; x") + .eval::("let /* I am a multi-line comment, yay! */ x = 5; x") .is_ok()); } diff --git a/tests/compound_equality.rs b/tests/compound_equality.rs index 569cfd94..b516bb97 100644 --- a/tests/compound_equality.rs +++ b/tests/compound_equality.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_or_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 16; x |= 74; x")?, 90); assert_eq!(engine.eval::("let x = true; x |= false; x")?, true); @@ -13,7 +13,7 @@ fn test_or_equals() -> Result<(), EvalAltResult> { #[test] fn test_and_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 16; x &= 31; x")?, 16); assert_eq!(engine.eval::("let x = true; x &= false; x")?, false); @@ -25,42 +25,42 @@ fn test_and_equals() -> Result<(), EvalAltResult> { #[test] fn test_xor_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 90; x ^= 12; x")?, 86); Ok(()) } #[test] fn test_multiply_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 2; x *= 3; x")?, 6); Ok(()) } #[test] fn test_divide_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 6; x /= 2; x")?, 3); Ok(()) } #[test] fn test_left_shift_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 9; x >>=1; x")?, 4); Ok(()) } #[test] fn test_right_shift_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 4; x<<= 2; x")?, 16); Ok(()) } #[test] fn test_modulo_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 10; x %= 4; x")?, 2); Ok(()) } diff --git a/tests/constants.rs b/tests/constants.rs index 16fbf7bd..2dd62000 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_constant() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("const x = 123; x")?, 123); diff --git a/tests/decrement.rs b/tests/decrement.rs index d653a7aa..a8cb2634 100644 --- a/tests/decrement.rs +++ b/tests/decrement.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_decrement() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 10; x -= 7; x")?, 3); diff --git a/tests/eval.rs b/tests/eval.rs index fc4424a4..bf6f750f 100644 --- a/tests/eval.rs +++ b/tests/eval.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, Scope, INT}; #[test] fn test_eval() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( @@ -19,7 +19,7 @@ fn test_eval() -> Result<(), EvalAltResult> { #[test] #[cfg(not(feature = "no_function"))] fn test_eval_function() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let mut scope = Scope::new(); assert_eq!( @@ -62,7 +62,7 @@ fn test_eval_function() -> Result<(), EvalAltResult> { #[test] #[cfg(not(feature = "no_function"))] fn test_eval_override() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( diff --git a/tests/expressions.rs b/tests/expressions.rs index 9023f97c..2091fe4e 100644 --- a/tests/expressions.rs +++ b/tests/expressions.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, Scope, INT}; #[test] fn test_expressions() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let mut scope = Scope::new(); scope.push("x", 10 as INT); diff --git a/tests/float.rs b/tests/float.rs index 20349098..94ec7a03 100644 --- a/tests/float.rs +++ b/tests/float.rs @@ -5,7 +5,7 @@ const EPSILON: FLOAT = 0.000_000_000_1; #[test] fn test_float() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::("let x = 0.0; let y = 1.0; x < y")?, diff --git a/tests/for.rs b/tests/for.rs index 448c7fcb..f36f3ae3 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -3,7 +3,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[cfg(not(feature = "no_index"))] #[test] fn test_for_array() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let script = r" let sum1 = 0; @@ -33,7 +33,7 @@ fn test_for_array() -> Result<(), EvalAltResult> { #[cfg(not(feature = "no_object"))] #[test] fn test_for_object() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let script = r#" let sum = 0; diff --git a/tests/if_block.rs b/tests/if_block.rs index 4699ee57..dcde8143 100644 --- a/tests/if_block.rs +++ b/tests/if_block.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_if() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("if true { 55 }")?, 55); assert_eq!(engine.eval::("if false { 55 } else { 44 }")?, 44); @@ -30,7 +30,7 @@ fn test_if() -> Result<(), EvalAltResult> { #[test] fn test_if_expr() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( diff --git a/tests/increment.rs b/tests/increment.rs index 72c6eb35..4be9397a 100644 --- a/tests/increment.rs +++ b/tests/increment.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_increment() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 1; x += 2; x")?, 3); assert_eq!( diff --git a/tests/internal_fn.rs b/tests/internal_fn.rs index 4a6720d5..cc99187a 100644 --- a/tests/internal_fn.rs +++ b/tests/internal_fn.rs @@ -4,9 +4,12 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_internal_fn() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); - assert_eq!(engine.eval::("fn addme(a, b) { a+b } addme(3, 4)")?, 7); + assert_eq!( + engine.eval::("fn add_me(a, b) { a+b } add_me(3, 4)")?, + 7 + ); assert_eq!(engine.eval::("fn bob() { return 4; 5 } bob()")?, 4); Ok(()) @@ -14,15 +17,15 @@ fn test_internal_fn() -> Result<(), EvalAltResult> { #[test] fn test_big_internal_fn() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( r" - fn mathme(a, b, c, d, e, f) { + fn math_me(a, b, c, d, e, f) { a - b * c + d * e - f } - mathme(100, 5, 2, 9, 6, 32) + math_me(100, 5, 2, 9, 6, 32) ", )?, 112 @@ -33,7 +36,7 @@ fn test_big_internal_fn() -> Result<(), EvalAltResult> { #[test] fn test_internal_fn_overloading() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( diff --git a/tests/looping.rs b/tests/looping.rs index bf2c1a72..c7aace27 100644 --- a/tests/looping.rs +++ b/tests/looping.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_loop() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( diff --git a/tests/maps.rs b/tests/maps.rs index 746be5cd..c256a1a8 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -4,7 +4,7 @@ use rhai::{AnyExt, Engine, EvalAltResult, Map, INT}; #[test] fn test_map_indexing() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); #[cfg(not(feature = "no_index"))] { @@ -75,7 +75,7 @@ fn test_map_indexing() -> Result<(), EvalAltResult> { #[test] fn test_map_assign() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let x = engine.eval::(r#"let x = #{a: 1, b: true, "c$": "hello"}; x"#)?; let a = x.get("a").cloned().expect("should have property a"); @@ -91,7 +91,7 @@ fn test_map_assign() -> Result<(), EvalAltResult> { #[test] fn test_map_return() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let x = engine.eval::(r#"#{a: 1, b: true, "c$": "hello"}"#)?; let a = x.get("a").cloned().expect("should have property a"); @@ -107,7 +107,7 @@ fn test_map_return() -> Result<(), EvalAltResult> { #[test] fn test_map_for() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( diff --git a/tests/math.rs b/tests/math.rs index 24181c5f..500b35b8 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_math() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("1 + 2")?, 3); assert_eq!(engine.eval::("1 - 2")?, -1); diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 5629f6cd..57cbb86e 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -3,7 +3,7 @@ use rhai::{Engine, EvalAltResult, RegisterFn, INT}; #[test] #[cfg(not(feature = "no_stdlib"))] fn test_mismatched_op() { - let mut engine = Engine::new(); + let engine = Engine::new(); assert!( matches!(engine.eval::(r#"60 + "hello""#).expect_err("expects error"), diff --git a/tests/not.rs b/tests/not.rs index 52913136..4ce9cd3e 100644 --- a/tests/not.rs +++ b/tests/not.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_not() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::("let not_true = !true; not_true")?, diff --git a/tests/number_literals.rs b/tests/number_literals.rs index f6466967..3fe8c78f 100644 --- a/tests/number_literals.rs +++ b/tests/number_literals.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_number_literal() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("65")?, 65); @@ -11,7 +11,7 @@ fn test_number_literal() -> Result<(), EvalAltResult> { #[test] fn test_hex_literal() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 0xf; x")?, 15); assert_eq!(engine.eval::("let x = 0xff; x")?, 255); @@ -21,7 +21,7 @@ fn test_hex_literal() -> Result<(), EvalAltResult> { #[test] fn test_octal_literal() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 0o77; x")?, 63); assert_eq!(engine.eval::("let x = 0o1234; x")?, 668); @@ -31,7 +31,7 @@ fn test_octal_literal() -> Result<(), EvalAltResult> { #[test] fn test_binary_literal() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 0b1111; x")?, 15); assert_eq!( diff --git a/tests/ops.rs b/tests/ops.rs index aaddea8a..c817d65d 100644 --- a/tests/ops.rs +++ b/tests/ops.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_ops() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("60 + 5")?, 65); assert_eq!(engine.eval::("(1 + 2) * (6 - 4) / 2")?, 3); @@ -11,8 +11,8 @@ fn test_ops() -> Result<(), EvalAltResult> { } #[test] -fn test_op_prec() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); +fn test_op_precedence() -> Result<(), EvalAltResult> { + let engine = Engine::new(); assert_eq!( engine.eval::("let x = 0; if x == 10 || true { x = 1} x")?, diff --git a/tests/power_of.rs b/tests/power_of.rs index b8b83f08..3762ff0f 100644 --- a/tests/power_of.rs +++ b/tests/power_of.rs @@ -8,7 +8,7 @@ const EPSILON: FLOAT = 0.000_000_000_1; #[test] fn test_power_of() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("2 ~ 3")?, 8); assert_eq!(engine.eval::("(-2 ~ 3)")?, -8); @@ -29,7 +29,7 @@ fn test_power_of() -> Result<(), EvalAltResult> { #[test] fn test_power_of_equals() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = 2; x ~= 3; x")?, 8); assert_eq!(engine.eval::("let x = -2; x ~= 3; x")?, -8); diff --git a/tests/side_effects.rs b/tests/side_effects.rs index d97b6f33..0375d1af 100644 --- a/tests/side_effects.rs +++ b/tests/side_effects.rs @@ -1,5 +1,3 @@ -#![cfg(not(feature = "no_object"))] - ///! This test simulates an external command object that is driven by a script. use rhai::{Engine, EvalAltResult, RegisterFn, Scope, INT}; use std::sync::{Arc, Mutex}; @@ -40,8 +38,9 @@ impl CommandWrapper { } } +#[cfg(not(feature = "no_object"))] #[test] -fn test_side_effects() -> Result<(), EvalAltResult> { +fn test_side_effects_command() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); let mut scope = Scope::new(); @@ -79,3 +78,22 @@ fn test_side_effects() -> Result<(), EvalAltResult> { Ok(()) } + +#[test] +fn test_side_effects_print() -> Result<(), EvalAltResult> { + use std::sync::RwLock; + + let result = RwLock::new(String::from("")); + + { + let mut engine = Engine::new(); + + // Override action of 'print' function + engine.on_print(|s| result.write().unwrap().push_str(s)); + + engine.consume("print(40 + 2);")?; + } + + assert_eq!(*result.read().unwrap(), "42"); + Ok(()) +} diff --git a/tests/string.rs b/tests/string.rs index 1ffd2e2e..3999df9e 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_string() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::(r#""Test string: \u2764""#)?, diff --git a/tests/throw.rs b/tests/throw.rs index 690d9c6e..6e55303c 100644 --- a/tests/throw.rs +++ b/tests/throw.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_throw() { - let mut engine = Engine::new(); + let engine = Engine::new(); assert!(matches!( engine.eval::<()>(r#"if true { throw "hello" }"#).expect_err("expects error"), diff --git a/tests/unary_after_binary.rs b/tests/unary_after_binary.rs index 16d4730a..ba74c9f9 100644 --- a/tests/unary_after_binary.rs +++ b/tests/unary_after_binary.rs @@ -4,7 +4,7 @@ use rhai::{Engine, EvalAltResult, INT}; // TODO also add test case for unary after compound // Hah, turns out unary + has a good use after all! fn test_unary_after_binary() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("10 % +4")?, 2); assert_eq!(engine.eval::("10 << +4")?, 160); diff --git a/tests/unary_minus.rs b/tests/unary_minus.rs index 80170498..e1a359d7 100644 --- a/tests/unary_minus.rs +++ b/tests/unary_minus.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_unary_minus() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = -5; x")?, -5); diff --git a/tests/unit.rs b/tests/unit.rs index 91e9d9f2..465c7b48 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -2,21 +2,21 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_unit() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); engine.eval::<()>("let x = (); x")?; Ok(()) } #[test] fn test_unit_eq() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!(engine.eval::("let x = (); let y = (); x == y")?, true); Ok(()) } #[test] fn test_unit_with_spaces() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); engine.eval::<()>("let x = ( ); x")?; Ok(()) } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 39cf5b76..8d5244c8 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, Scope, INT}; #[test] fn test_var_scope() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); let mut scope = Scope::new(); engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; @@ -21,7 +21,7 @@ fn test_var_scope() -> Result<(), EvalAltResult> { #[test] fn test_scope_eval() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); // First create the state let mut scope = Scope::new(); diff --git a/tests/while_loop.rs b/tests/while_loop.rs index 18d5a03b..3a035039 100644 --- a/tests/while_loop.rs +++ b/tests/while_loop.rs @@ -2,7 +2,7 @@ use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_while() -> Result<(), EvalAltResult> { - let mut engine = Engine::new(); + let engine = Engine::new(); assert_eq!( engine.eval::( From 9f3646d9ecf53cafb4c64352a5d3993f06926c88 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 7 Apr 2020 21:50:33 +0800 Subject: [PATCH 04/21] Keep only one call_fn, adds tuples of one and zero. --- README.md | 22 +++++------ src/api.rs | 101 +++++++---------------------------------------- src/call.rs | 1 + tests/call_fn.rs | 4 +- 4 files changed, 27 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 4b5c239e..8cfe5444 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,7 @@ Compiling a script file is also supported: let ast = engine.compile_file("hello_world.rhai".into())?; ``` -Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust - -via `call_fn` or its cousins `call_fn1` (one argument) and `call_fn0` (no argument). +Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`. ```rust // Define functions in a script. @@ -239,20 +238,19 @@ let ast = engine.compile(true, // A custom scope can also contain any variables/constants available to the functions let mut scope = Scope::new(); -// Evaluate a function defined in the script, 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 arguments of the wrong types are passed, -// the Engine will not find the function. +// Evaluate a function defined in the script, passing arguments into the script as a tuple. +// Beware, arguments must be of the correct types because 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(&mut scope, &ast, "hello", ( String::from("abc"), 123_i64 ) )?; // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // put arguments in a tuple -let result: i64 = engine.call_fn1(&mut scope, &ast, "hello", 123_i64)? -// ^^^^^^^^ use 'call_fn1' for one argument +let result: i64 = engine.call_fn(&mut scope, &ast, "hello", (123_i64,) )? +// ^^^^^^^^^^ tuple of one -let result: i64 = engine.call_fn0(&mut scope, &ast, "hello")? -// ^^^^^^^^ use 'call_fn0' for no arguments +let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )? +// ^^ unit = tuple of zero ``` Evaluate expressions only @@ -305,8 +303,8 @@ they even cannot be added together. This is very similar to Rust. 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 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 some 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 some 32-bit systems where using 64-bit integers incurs a performance penalty. If no floating-point is needed or supported, use the [`no_float`] feature to remove it. diff --git a/src/api.rs b/src/api.rs index 461c97e9..6df0cc8d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -831,79 +831,6 @@ impl<'e> Engine<'e> { }) } - /// Call a script function defined in an `AST` with no argument. - /// - /// # Example - /// - /// ``` - /// # fn main() -> Result<(), rhai::EvalAltResult> { - /// # #[cfg(not(feature = "no_stdlib"))] - /// # #[cfg(not(feature = "no_function"))] - /// # { - /// use rhai::{Engine, Scope}; - /// - /// let engine = Engine::new(); - /// - /// let ast = engine.compile("fn num() { 42 + foo }")?; - /// - /// let mut scope = Scope::new(); - /// scope.push("foo", 42_i64); - /// - /// // Call the script-defined function - /// let result: i64 = engine.call_fn0(&mut scope, &ast, "num")?; - /// - /// assert_eq!(result, 84); - /// # } - /// # Ok(()) - /// # } - /// ``` - #[cfg(not(feature = "no_function"))] - pub fn call_fn0( - &self, - scope: &mut Scope, - ast: &AST, - name: &str, - ) -> Result { - self.call_fn_internal(scope, ast, name, vec![]) - } - - /// Call a script function defined in an `AST` with one argument. - /// - /// # Example - /// - /// ``` - /// # fn main() -> Result<(), rhai::EvalAltResult> { - /// # #[cfg(not(feature = "no_stdlib"))] - /// # #[cfg(not(feature = "no_function"))] - /// # { - /// use rhai::{Engine, Scope}; - /// - /// let engine = Engine::new(); - /// - /// let ast = engine.compile("fn inc(x) { x + foo }")?; - /// - /// let mut scope = Scope::new(); - /// scope.push("foo", 42_i64); - /// - /// // Call the script-defined function - /// let result: i64 = engine.call_fn1(&mut scope, &ast, "inc", 123_i64)?; - /// - /// assert_eq!(result, 165); - /// # } - /// # Ok(()) - /// # } - /// ``` - #[cfg(not(feature = "no_function"))] - pub fn call_fn1( - &self, - scope: &mut Scope, - ast: &AST, - name: &str, - arg: A, - ) -> Result { - self.call_fn_internal(scope, ast, name, vec![arg.into_dynamic()]) - } - /// Call a script function defined in an `AST` with multiple arguments. /// /// # Example @@ -917,15 +844,25 @@ impl<'e> Engine<'e> { /// /// let engine = Engine::new(); /// - /// let ast = engine.compile("fn add(x, y) { len(x) + y + foo }")?; + /// let ast = engine.compile(r" + /// fn add(x, y) { len(x) + y + foo } + /// fn add1(x) { len(x) + 1 + foo } + /// fn bar() { foo/2 } + /// ")?; /// /// let mut scope = Scope::new(); /// scope.push("foo", 42_i64); /// /// // Call the script-defined function - /// let result: i64 = engine.call_fn(&mut scope, &ast, "add", (String::from("abc"), 123_i64))?; - /// + /// let result: i64 = engine.call_fn(&mut scope, &ast, "add", ( String::from("abc"), 123_i64 ) )?; /// assert_eq!(result, 168); + /// + /// let result: i64 = engine.call_fn(&mut scope, &ast, "add1", ( String::from("abc"), ) )?; + /// // ^^^^^^^^^^^^^^^^^^^^^^^^ tuple of one + /// assert_eq!(result, 46); + /// + /// let result: i64 = engine.call_fn(&mut scope, &ast, "bar", () )?; + /// assert_eq!(result, 21); /// # } /// # Ok(()) /// # } @@ -938,17 +875,7 @@ impl<'e> Engine<'e> { name: &str, args: A, ) -> Result { - self.call_fn_internal(scope, ast, name, args.into_vec()) - } - - #[cfg(not(feature = "no_function"))] - fn call_fn_internal( - &self, - scope: &mut Scope, - ast: &AST, - name: &str, - mut arg_values: Vec, - ) -> Result { + let mut arg_values = args.into_vec(); let mut args: Vec<_> = arg_values.iter_mut().map(Dynamic::as_mut).collect(); let fn_lib = Some(ast.1.as_ref()); let pos = Position::none(); diff --git a/src/call.rs b/src/call.rs index ac33e2c7..c81b25bc 100644 --- a/src/call.rs +++ b/src/call.rs @@ -36,6 +36,7 @@ macro_rules! impl_args { (@pop) => { }; (@pop $head:ident) => { + impl_args!(); }; (@pop $head:ident $(, $tail:ident)+) => { impl_args!($($tail),*); diff --git a/tests/call_fn.rs b/tests/call_fn.rs index b0b3793d..4ee2c3a6 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -44,10 +44,10 @@ fn test_call_fn() -> Result<(), EvalAltResult> { let r: i64 = engine.call_fn(&mut scope, &ast, "hello", (42 as INT, 123 as INT))?; assert_eq!(r, 165); - let r: i64 = engine.call_fn1(&mut scope, &ast, "hello", 123 as INT)?; + let r: i64 = engine.call_fn(&mut scope, &ast, "hello", (123 as INT,))?; assert_eq!(r, 5166); - let r: i64 = engine.call_fn0(&mut scope, &ast, "hello")?; + let r: i64 = engine.call_fn(&mut scope, &ast, "hello", ())?; assert_eq!(r, 42); assert_eq!( From b74c85f04cbd3a4408714326f5a02e44a520648b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 7 Apr 2020 23:13:47 +0800 Subject: [PATCH 05/21] Fix max call depth and add test. --- src/engine.rs | 14 +++++++++++++- tests/stack.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/stack.rs diff --git a/src/engine.rs b/src/engine.rs index e0ce5c43..e24b2ee8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -49,7 +49,12 @@ type IteratorFn = dyn Fn(&Dynamic) -> Box> + Send + #[cfg(not(feature = "sync"))] type IteratorFn = dyn Fn(&Dynamic) -> Box>; -pub const MAX_CALL_STACK_DEPTH: usize = 64; +#[cfg(debug_assertions)] +pub const MAX_CALL_STACK_DEPTH: usize = 42; + +#[cfg(not(debug_assertions))] +pub const MAX_CALL_STACK_DEPTH: usize = 256; + pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_DUMP_AST: &str = "dump_ast"; @@ -271,6 +276,8 @@ pub struct Engine<'e> { pub(crate) optimization_level: OptimizationLevel, /// Maximum levels of call-stack to prevent infinite recursion. + /// + /// Defaults to 42 for debug builds and 256 for non-debug builds. pub(crate) max_call_stack_depth: usize, } @@ -436,6 +443,11 @@ impl Engine<'_> { pos: Position, level: usize, ) -> Result { + // Check for stack overflow + if level > self.max_call_stack_depth { + return Err(EvalAltResult::ErrorStackOverflow(pos)); + } + // First search in script-defined functions (can override built-in) if let Some(lib) = fn_lib { if let Some(fn_def) = lib.get_function(fn_name, args.len()) { diff --git a/tests/stack.rs b/tests/stack.rs new file mode 100644 index 00000000..62964c9c --- /dev/null +++ b/tests/stack.rs @@ -0,0 +1,29 @@ +use rhai::{Engine, EvalAltResult}; + +#[test] +fn test_stack_overflow() -> Result<(), EvalAltResult> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r" + fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } } + foo(38) + ", + )?, + 741 + ); + + match engine.eval::<()>( + r" + fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } } + foo(1000) + ", + ) { + Ok(_) => panic!("should be stack overflow"), + Err(EvalAltResult::ErrorStackOverflow(_)) => (), + Err(_) => panic!("should be stack overflow"), + } + + Ok(()) +} From e0bb2e5c97e1e40f22398f8cdfdae940b703e0cb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Apr 2020 09:30:50 +0800 Subject: [PATCH 06/21] Change optimize_ast to take optimization level as parameter. --- README.md | 13 +++++++++++++ examples/repl.rs | 4 +--- src/api.rs | 11 ++++++++--- src/optimize.rs | 26 ++++++++++++++++++-------- src/parser.rs | 16 ++++++++++++++-- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8cfe5444..c6ec7f48 100644 --- a/README.md +++ b/README.md @@ -1753,6 +1753,19 @@ An [`Engine`]'s optimization level is set via a call to `set_optimization_level` engine.set_optimization_level(rhai::OptimizationLevel::Full); ``` +If it is ever needed to _re_-optimize an `AST`, use the `optimize_ast` method. + +```rust +// Compile script to AST +let ast = engine.compile("40 + 2")?; + +// Create a new 'Scope' - put constants in it to aid optimization if using 'OptimizationLevel::Full' +let scope = Scope::new(); + +// Re-optimize the AST +let ast = engine.optimize_ast(&scope, &ast, OptimizationLevel::Full); +``` + When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_ evaluated all function calls with constant arguments, using the result to replace the call. This also applies to all operators (which are implemented as functions). For instance, the same example above: diff --git a/examples/repl.rs b/examples/repl.rs index d4679842..56f0ac69 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -145,9 +145,7 @@ fn main() { #[cfg(not(feature = "no_optimize"))] { - engine.set_optimization_level(OptimizationLevel::Full); - ast = engine.optimize_ast(&scope, r); - engine.set_optimization_level(OptimizationLevel::None); + ast = engine.optimize_ast(&scope, r, OptimizationLevel::Full); } #[cfg(feature = "no_optimize")] diff --git a/src/api.rs b/src/api.rs index 6df0cc8d..55943f24 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,7 +10,7 @@ use crate::result::EvalAltResult; use crate::scope::Scope; #[cfg(not(feature = "no_optimize"))] -use crate::optimize::optimize_into_ast; +use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::stdlib::{ any::{type_name, TypeId}, @@ -902,9 +902,14 @@ impl<'e> Engine<'e> { /// 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 { + pub fn optimize_ast( + &self, + scope: &Scope, + ast: AST, + optimization_level: OptimizationLevel, + ) -> AST { let fn_lib = ast.1.iter().map(|fn_def| fn_def.as_ref().clone()).collect(); - optimize_into_ast(self, scope, ast.0, fn_lib) + optimize_into_ast(self, scope, ast.0, fn_lib, optimization_level) } /// Override default action of `print` (print to stdout using `println!`) diff --git a/src/optimize.rs b/src/optimize.rs index 85c61d1e..74b77395 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -41,16 +41,23 @@ struct State<'a> { engine: &'a Engine<'a>, /// Library of script-defined functions. fn_lib: &'a [(&'a str, usize)], + /// Optimization level. + optimization_level: OptimizationLevel, } impl<'a> State<'a> { /// Create a new State. - pub fn new(engine: &'a Engine<'a>, fn_lib: &'a [(&'a str, usize)]) -> Self { + pub fn new( + engine: &'a Engine<'a>, + fn_lib: &'a [(&'a str, usize)], + level: OptimizationLevel, + ) -> Self { Self { changed: false, constants: vec![], engine, fn_lib, + optimization_level: level, } } /// Reset the state from dirty to clean. @@ -501,7 +508,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { // Eagerly call functions Expr::FunctionCall(id, args, def_value, pos) - if state.engine.optimization_level == OptimizationLevel::Full // full optimizations + if state.optimization_level == OptimizationLevel::Full // full optimizations && args.iter().all(|expr| expr.is_constant()) // all arguments are constants => { // First search in script-defined functions (can override built-in) @@ -560,14 +567,15 @@ pub(crate) fn optimize<'a>( engine: &Engine<'a>, scope: &Scope, fn_lib: &'a [(&'a str, usize)], + level: OptimizationLevel, ) -> Vec { // If optimization level is None then skip optimizing - if engine.optimization_level == OptimizationLevel::None { + if level == OptimizationLevel::None { return statements; } // Set up the state - let mut state = State::new(engine, fn_lib); + let mut state = State::new(engine, fn_lib, level); // Add constants from the scope into the state scope @@ -640,6 +648,7 @@ pub fn optimize_into_ast( scope: &Scope, statements: Vec, functions: Vec, + level: OptimizationLevel, ) -> AST { let fn_lib: Vec<_> = functions .iter() @@ -651,11 +660,12 @@ pub fn optimize_into_ast( .iter() .cloned() .map(|mut fn_def| { - if engine.optimization_level != OptimizationLevel::None { + if level != OptimizationLevel::None { let pos = fn_def.body.position(); // Optimize the function body - let mut body = optimize(vec![fn_def.body], engine, &Scope::new(), &fn_lib); + let mut body = + optimize(vec![fn_def.body], engine, &Scope::new(), &fn_lib, level); // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { @@ -675,10 +685,10 @@ pub fn optimize_into_ast( ); AST( - match engine.optimization_level { + match level { OptimizationLevel::None => statements, OptimizationLevel::Simple | OptimizationLevel::Full => { - optimize(statements, engine, &scope, &fn_lib) + optimize(statements, engine, &scope, &fn_lib, level) } }, #[cfg(feature = "sync")] diff --git a/src/parser.rs b/src/parser.rs index 98a3aff3..c5a1041e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2781,7 +2781,13 @@ pub fn parse_global_expr<'a, 'e>( Ok( // Optimize AST #[cfg(not(feature = "no_optimize"))] - optimize_into_ast(engine, scope, vec![Stmt::Expr(Box::new(expr))], vec![]), + optimize_into_ast( + engine, + scope, + vec![Stmt::Expr(Box::new(expr))], + vec![], + engine.optimization_level, + ), // // Do not optimize AST if `no_optimize` #[cfg(feature = "no_optimize")] @@ -2866,7 +2872,13 @@ pub fn parse<'a, 'e>( Ok( // Optimize AST #[cfg(not(feature = "no_optimize"))] - optimize_into_ast(engine, scope, statements, functions), + optimize_into_ast( + engine, + scope, + statements, + functions, + engine.optimization_level, + ), // // Do not optimize AST if `no_optimize` #[cfg(feature = "no_optimize")] From bcff6bfd712d3ed151abe33b50c9197a158a73ec Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Apr 2020 10:19:03 +0800 Subject: [PATCH 07/21] Remove no_stdlib feature in favor of Engine::new_raw(). --- Cargo.toml | 8 +++-- README.md | 73 +++++++++++++++++++++++-------------- src/api.rs | 35 +++++++++++++++--- src/builtin.rs | 3 +- src/engine.rs | 82 ++++++++++++++++++++++++++---------------- src/lib.rs | 1 - src/parser.rs | 11 ++---- tests/arrays.rs | 43 +++++++++++----------- tests/maps.rs | 61 +++++++++++++++---------------- tests/mismatched_op.rs | 3 +- tests/stack.rs | 1 + tests/string.rs | 6 ++-- 12 files changed, 191 insertions(+), 136 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 43339787..632018f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,16 +20,14 @@ categories = [ "no-std", "embedded", "parser-implementations" ] num-traits = "*" [features] -#default = ["no_function", "no_index", "no_object", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize", "sync"] +#default = ["no_stdlib", "no_function", "no_index", "no_object", "no_float", "only_i32", "unchecked", "no_optimize", "sync"] default = [] unchecked = [] # unchecked arithmetic -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_object = [] # no custom objects no_optimize = [] # no script optimizer -optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simplify testing only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types sync = [] # restrict to only types that implement Send + Sync @@ -37,6 +35,10 @@ sync = [] # restrict to only types that implement Send + Sync # compiling for no-std no_std = [ "num-traits/libm", "hashbrown", "core-error", "libm" ] +# other developer features +no_stdlib = [] # do not register the standard library +optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simplify testing + [profile.release] lto = "fat" codegen-units = 1 diff --git a/README.md b/README.md index c6ec7f48..90609c5a 100644 --- a/README.md +++ b/README.md @@ -59,26 +59,24 @@ Beware that in order to use pre-releases (e.g. alpha and beta), the exact versio Optional features ----------------- -| Feature | Description | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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 not needed. | -| `no_index` | Disable arrays and indexing features if not needed. | -| `no_object` | Disable support for custom types and objects. | -| `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. `INT` is set to `i32`. | -| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | -| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | -| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. | +| Feature | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `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 not needed. | +| `no_index` | Disable arrays and indexing features if not needed. | +| `no_object` | Disable support for custom types and objects. | +| `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. `INT` is set to `i32`. | +| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | +| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | +| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. | 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. [`unchecked`]: #optional-features -[`no_stdlib`]: #optional-features [`no_index`]: #optional-features [`no_float`]: #optional-features [`no_function`]: #optional-features @@ -160,7 +158,7 @@ Hello world [`Engine`]: #hello-world -To get going with Rhai, create an instance of the scripting engine and then call `eval`: +To get going with Rhai, create an instance of the scripting engine via `Engine::new` and then call the `eval` method: ```rust use rhai::{Engine, EvalAltResult}; @@ -171,7 +169,7 @@ fn main() -> Result<(), EvalAltResult> let result = engine.eval::("40 + 2")?; - println!("Answer: {}", result); // prints 42 + println!("Answer: {}", result); // prints 42 Ok(()) } @@ -203,7 +201,7 @@ let ast = engine.compile("40 + 2")?; for _ in 0..42 { let result: i64 = engine.eval_ast(&ast)?; - println!("Answer #{}: {}", i, result); // prints 42 +println!("Answer #{}: {}", i, result); // prints 42 } ``` @@ -253,6 +251,27 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )? // ^^ unit = tuple of zero ``` +Raw `Engine` +------------ + +[raw `Engine`]: #raw-engine + +`Engine::new` creates a scripting [`Engine`] with common functionalities (e.g. printing to the console via `print`). +In many controlled embedded environments, however, these are not needed. + +Use `Engine::new_raw` to create a _raw_ `Engine`, in which: + +* the `print` and `debug` statements do nothing instead of displaying to the console (see [`print` and `debug`](#print-and-debug) below) +* the _standard library_ of utility functions is _not_ loaded by default (load it using the `register_stdlib` method). + +```rust +let mut engine = Engine::new_raw(); // Create a 'raw' Engine + +engine.register_stdlib(); // Register the standard library manually + +engine. +``` + Evaluate expressions only ------------------------- @@ -263,8 +282,8 @@ In these cases, use the `compile_expression` and `eval_expression` methods or th let result = engine.eval_expression::("2 + (10 + 10) * 2")?; ``` -When evaluation _expressions_, no control-flow statement (e.g. `if`, `while`, `for`) is not supported and will be -parse errors when encountered - not even variable assignments. +When evaluation _expressions_, no full-blown statement (e.g. `if`, `while`, `for`) - not even variable assignments - +is supported and will be considered parse errors when encountered. ```rust // The following are all syntax errors because the script is not an expression. @@ -955,7 +974,7 @@ number = -5 - +5; Numeric functions ----------------- -The following standard functions (defined in the standard library but excluded if [`no_stdlib`]) operate on +The following standard functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: | Function | Description | @@ -966,7 +985,7 @@ The following standard functions (defined in the standard library but excluded i Floating-point functions ------------------------ -The following standard functions (defined in the standard library but excluded if [`no_stdlib`]) operate on `f64` only: +The following standard functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on `f64` only: | Category | Functions | | ---------------- | ------------------------------------------------------------ | @@ -996,7 +1015,7 @@ Individual characters within a Rhai string can also be replaced just as if the s In Rhai, there is also 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. +if using a [raw `Engine`]). This is particularly useful when printing output. [`type_of()`] a string returns `"string"`. @@ -1044,7 +1063,7 @@ record == "Bob X. Davis: age 42 ❤\n"; 'C' in record == false; ``` -The following standard functions (defined in the standard library but excluded if [`no_stdlib`]) operate on strings: +The following standard functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on strings: | Function | Description | | ---------- | ------------------------------------------------------------------------ | @@ -1097,7 +1116,7 @@ The Rust type of a Rhai array is `rhai::Array`. [`type_of()`] an array returns ` Arrays are disabled via the [`no_index`] feature. -The following functions (defined in the standard library but excluded if [`no_stdlib`]) operate on arrays: +The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on arrays: | Function | Description | | ------------ | ------------------------------------------------------------------------------------- | @@ -1193,7 +1212,7 @@ The Rust type of a Rhai object map is `rhai::Map`. [`type_of()`] an object map r Object maps are disabled via the [`no_object`] feature. -The following functions (defined in the standard library but excluded if [`no_stdlib`]) operate on object maps: +The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on object maps: | Function | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | @@ -1271,7 +1290,7 @@ Comparison operators 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 - +However, if using a [raw `Engine`], 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 @@ -1753,7 +1772,7 @@ An [`Engine`]'s optimization level is set via a call to `set_optimization_level` engine.set_optimization_level(rhai::OptimizationLevel::Full); ``` -If it is ever needed to _re_-optimize an `AST`, use the `optimize_ast` method. +If it is ever needed to _re_-optimize an `AST`, use the `optimize_ast` method: ```rust // Compile script to AST diff --git a/src/api.rs b/src/api.rs index 55943f24..db3a9859 100644 --- a/src/api.rs +++ b/src/api.rs @@ -387,8 +387,30 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn compile_with_scope(&self, scope: &Scope, input: &str) -> Result { + self.compile_with_scope_and_optimization_level( + scope, + input, + #[cfg(not(feature = "no_optimize"))] + self.optimization_level, + ) + } + + /// Compile a string into an `AST` using own scope at a specific optimization level. + pub(crate) fn compile_with_scope_and_optimization_level( + &self, + scope: &Scope, + input: &str, + #[cfg(not(feature = "no_optimize"))] optimization_level: OptimizationLevel, + ) -> Result { let tokens_stream = lex(input); - parse(&mut tokens_stream.peekable(), self, scope) + + parse( + &mut tokens_stream.peekable(), + self, + scope, + #[cfg(not(feature = "no_optimize"))] + optimization_level, + ) } /// Read the contents of a file into a string. @@ -800,8 +822,14 @@ impl<'e> Engine<'e> { pub fn consume_with_scope(&self, scope: &mut Scope, input: &str) -> Result<(), EvalAltResult> { let tokens_stream = lex(input); - let ast = parse(&mut tokens_stream.peekable(), self, scope) - .map_err(EvalAltResult::ErrorParsing)?; + let ast = parse( + &mut tokens_stream.peekable(), + self, + scope, + #[cfg(not(feature = "no_optimize"))] + self.optimization_level, + ) + .map_err(EvalAltResult::ErrorParsing)?; self.consume_ast_with_scope(scope, &ast) } @@ -837,7 +865,6 @@ impl<'e> Engine<'e> { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { - /// # #[cfg(not(feature = "no_stdlib"))] /// # #[cfg(not(feature = "no_function"))] /// # { /// use rhai::{Engine, Scope}; diff --git a/src/builtin.rs b/src/builtin.rs index b6baed63..f63f4fd5 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -757,8 +757,7 @@ macro_rules! reg_fn2y { /// Register the built-in library. impl Engine<'_> { - #[cfg(not(feature = "no_stdlib"))] - pub(crate) fn register_stdlib(&mut self) { + pub fn register_stdlib(&mut self) { #[cfg(not(feature = "no_float"))] { // Advanced math functions diff --git a/src/engine.rs b/src/engine.rs index e24b2ee8..f351f72e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -283,28 +283,25 @@ pub struct Engine<'e> { impl Default for Engine<'_> { fn default() -> Self { - // User-friendly names for built-in types - let type_names = [ - #[cfg(not(feature = "no_index"))] - (type_name::(), "array"), - #[cfg(not(feature = "no_object"))] - (type_name::(), "map"), - (type_name::(), "string"), - (type_name::(), "dynamic"), - (type_name::(), "variant"), - ] - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - // Create the new scripting Engine let mut engine = Engine { functions: None, type_iterators: None, - type_names: Some(type_names), - on_print: Some(Box::new(default_print)), // default print/debug implementations + type_names: None, + + // default print/debug implementations + #[cfg(not(feature = "no_std"))] + on_print: Some(Box::new(default_print)), + #[cfg(not(feature = "no_std"))] on_debug: Some(Box::new(default_print)), + // default print/debug implementations + #[cfg(feature = "no_std")] + on_print: None, + #[cfg(feature = "no_std")] + on_debug: None, + + // optimization level #[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "optimize_full"))] optimization_level: OptimizationLevel::Simple, @@ -316,10 +313,11 @@ impl Default for Engine<'_> { max_call_stack_depth: MAX_CALL_STACK_DEPTH, }; + engine.fill_type_names(); engine.register_core_lib(); #[cfg(not(feature = "no_stdlib"))] - engine.register_stdlib(); // Register the standard library when no_stdlib is not set + engine.register_stdlib(); engine } @@ -354,6 +352,24 @@ fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { } impl Engine<'_> { + fn fill_type_names(&mut self) { + // User-friendly names for built-in types + self.type_names = Some( + [ + #[cfg(not(feature = "no_index"))] + (type_name::(), "array"), + #[cfg(not(feature = "no_object"))] + (type_name::(), "map"), + (type_name::(), "string"), + (type_name::(), "dynamic"), + (type_name::(), "variant"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + ); + } + /// Create a new `Engine` pub fn new() -> Self { // fn abc(f: F) { @@ -364,7 +380,7 @@ impl Engine<'_> { Default::default() } - /// Create a new `Engine` with minimal configurations - i.e. without pretty-print type names etc. + /// Create a new `Engine` with minimal configurations without the standard library etc. pub fn new_raw() -> Self { let mut engine = Engine { functions: None, @@ -384,11 +400,9 @@ impl Engine<'_> { max_call_stack_depth: MAX_CALL_STACK_DEPTH, }; + engine.fill_type_names(); engine.register_core_lib(); - #[cfg(not(feature = "no_stdlib"))] - engine.register_stdlib(); // Register the standard library when no_stdlib is not set - engine } @@ -1487,13 +1501,24 @@ impl Engine<'_> { })?; // Compile the script text - let mut ast = self.compile(script).map_err(EvalAltResult::ErrorParsing)?; + // No optimizations because we only run it once + let mut ast = self + .compile_with_scope_and_optimization_level( + &Scope::new(), + script, + #[cfg(not(feature = "no_optimize"))] + OptimizationLevel::None, + ) + .map_err(EvalAltResult::ErrorParsing)?; // If new functions are defined within the eval string, it is an error - if ast.1.len() > 0 { - return Err(EvalAltResult::ErrorParsing( - ParseErrorType::WrongFnDefinition.into_err(pos), - )); + #[cfg(not(feature = "no_function"))] + { + if ast.1.len() > 0 { + return Err(EvalAltResult::ErrorParsing( + ParseErrorType::WrongFnDefinition.into_err(pos), + )); + } } if let Some(lib) = fn_lib { @@ -1760,11 +1785,6 @@ impl Engine<'_> { /// Print/debug to stdout #[cfg(not(feature = "no_std"))] -#[cfg(not(feature = "no_stdlib"))] fn default_print(s: &str) { println!("{}", s); } - -/// No-op -#[cfg(any(feature = "no_std", feature = "no_stdlib"))] -fn default_print(_: &str) {} diff --git a/src/lib.rs b/src/lib.rs index dba21d7a..b2982f08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,6 @@ //! //! | Feature | Description | //! | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -//! | `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 not needed. | //! | `no_index` | Disable arrays and indexing features if not needed. | diff --git a/src/parser.rs b/src/parser.rs index c5a1041e..569d0ac4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6,7 +6,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::scope::{EntryType as ScopeEntryType, Scope}; #[cfg(not(feature = "no_optimize"))] -use crate::optimize::optimize_into_ast; +use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::stdlib::{ borrow::Cow, @@ -2866,19 +2866,14 @@ pub fn parse<'a, 'e>( input: &mut Peekable>, engine: &Engine<'e>, scope: &Scope, + #[cfg(not(feature = "no_optimize"))] optimization_level: OptimizationLevel, ) -> Result { let (statements, functions) = parse_global_level(input)?; Ok( // Optimize AST #[cfg(not(feature = "no_optimize"))] - optimize_into_ast( - engine, - scope, - statements, - functions, - engine.optimization_level, - ), + optimize_into_ast(engine, scope, statements, functions, optimization_level), // // Do not optimize AST if `no_optimize` #[cfg(feature = "no_optimize")] diff --git a/tests/arrays.rs b/tests/arrays.rs index e1dc066a..33568460 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -13,42 +13,39 @@ fn test_arrays() -> Result<(), EvalAltResult> { ); assert!(engine.eval::("let y = [1, 2, 3]; 2 in y")?); - #[cfg(not(feature = "no_stdlib"))] - { - assert_eq!( - engine.eval::( - r" + assert_eq!( + engine.eval::( + r" let x = [1, 2, 3]; let y = [4, 5]; x.append(y); x.len() " - )?, - 5 - ); - assert_eq!( - engine.eval::( - r" + )?, + 5 + ); + assert_eq!( + engine.eval::( + r" let x = [1, 2, 3]; x += [4, 5]; x.len() " - )?, - 5 - ); - assert_eq!( - engine - .eval::( - r" + )?, + 5 + ); + assert_eq!( + engine + .eval::( + r" let x = [1, 2, 3]; let y = [4, 5]; x + y " - )? - .len(), - 5 - ); - } + )? + .len(), + 5 + ); Ok(()) } diff --git a/tests/maps.rs b/tests/maps.rs index c256a1a8..b979f331 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -33,42 +33,39 @@ fn test_map_indexing() -> Result<(), EvalAltResult> { assert!(engine.eval::("let y = #{a: 1, b: 2, c: 3}; 'b' in y")?); assert!(!engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "z" in y"#)?); - #[cfg(not(feature = "no_stdlib"))] - { - assert_eq!( - engine.eval::( + assert_eq!( + engine.eval::( + r" + let x = #{a: 1, b: 2, c: 3}; + let y = #{b: 42, d: 9}; + x.mixin(y); + x.len() + x.b + " + )?, + 46 + ); + assert_eq!( + engine.eval::( + r" + let x = #{a: 1, b: 2, c: 3}; + x += #{b: 42, d: 9}; + x.len() + x.b + " + )?, + 46 + ); + assert_eq!( + engine + .eval::( r" let x = #{a: 1, b: 2, c: 3}; let y = #{b: 42, d: 9}; - x.mixin(y); - x.len() + x.b + x + y " - )?, - 46 - ); - assert_eq!( - engine.eval::( - r" - let x = #{a: 1, b: 2, c: 3}; - x += #{b: 42, d: 9}; - x.len() + x.b - " - )?, - 46 - ); - assert_eq!( - engine - .eval::( - r" - let x = #{a: 1, b: 2, c: 3}; - let y = #{b: 42, d: 9}; - x + y - " - )? - .len(), - 4 - ); - } + )? + .len(), + 4 + ); Ok(()) } diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 57cbb86e..e6e6e15d 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -1,12 +1,11 @@ use rhai::{Engine, EvalAltResult, RegisterFn, INT}; #[test] -#[cfg(not(feature = "no_stdlib"))] fn test_mismatched_op() { let engine = Engine::new(); assert!( - matches!(engine.eval::(r#"60 + "hello""#).expect_err("expects error"), + matches!(engine.eval::(r#""hello, " + "world!""#).expect_err("expects error"), EvalAltResult::ErrorMismatchOutputType(err, _) if err == "string") ); } diff --git a/tests/stack.rs b/tests/stack.rs index 62964c9c..feee09d5 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -1,3 +1,4 @@ +#![cfg(not(feature = "no_function"))] use rhai::{Engine, EvalAltResult}; #[test] diff --git a/tests/string.rs b/tests/string.rs index 3999df9e..3eeb3a35 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -22,12 +22,12 @@ fn test_string() -> Result<(), EvalAltResult> { #[cfg(not(feature = "no_stdlib"))] assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); + #[cfg(not(feature = "no_stdlib"))] + assert_eq!(engine.eval::("(42).to_string()")?, "42"); + #[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_stdlib"))] assert_eq!(engine.eval::(r#""foo" + 123.4556"#)?, "foo123.4556"); - #[cfg(not(feature = "no_stdlib"))] - assert_eq!(engine.eval::("(42).to_string()")?, "42"); - Ok(()) } From 660ce6cc79a48e1750b02e2c2498073427d14bc7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Apr 2020 16:57:15 +0800 Subject: [PATCH 08/21] Change parameter input to script. --- src/api.rs | 54 ++++++++++++++++++++++++------------------------------ 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/api.rs b/src/api.rs index db3a9859..a33526d2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -345,8 +345,8 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` - pub fn compile(&self, input: &str) -> Result { - self.compile_with_scope(&Scope::new(), input) + pub fn compile(&self, script: &str) -> Result { + self.compile_with_scope(&Scope::new(), script) } /// Compile a string into an `AST` using own scope, which can be used later for evaluation. @@ -386,10 +386,10 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` - pub fn compile_with_scope(&self, scope: &Scope, input: &str) -> Result { + pub fn compile_with_scope(&self, scope: &Scope, script: &str) -> Result { self.compile_with_scope_and_optimization_level( scope, - input, + script, #[cfg(not(feature = "no_optimize"))] self.optimization_level, ) @@ -399,10 +399,10 @@ impl<'e> Engine<'e> { pub(crate) fn compile_with_scope_and_optimization_level( &self, scope: &Scope, - input: &str, + script: &str, #[cfg(not(feature = "no_optimize"))] optimization_level: OptimizationLevel, ) -> Result { - let tokens_stream = lex(input); + let tokens_stream = lex(script); parse( &mut tokens_stream.peekable(), @@ -488,10 +488,7 @@ impl<'e> Engine<'e> { scope: &Scope, path: PathBuf, ) -> Result { - Self::read_file(path).and_then(|contents| { - self.compile_with_scope(scope, &contents) - .map_err(|err| err.into()) - }) + Self::read_file(path).and_then(|contents| Ok(self.compile_with_scope(scope, &contents)?)) } /// Compile a string containing an expression into an `AST`, @@ -514,8 +511,8 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` - pub fn compile_expression(&self, input: &str) -> Result { - self.compile_expression_with_scope(&Scope::new(), input) + pub fn compile_expression(&self, script: &str) -> Result { + self.compile_expression_with_scope(&Scope::new(), script) } /// Compile a string containing an expression into an `AST` using own scope, @@ -560,9 +557,9 @@ impl<'e> Engine<'e> { pub fn compile_expression_with_scope( &self, scope: &Scope, - input: &str, + script: &str, ) -> Result { - let tokens_stream = lex(input); + let tokens_stream = lex(script); parse_global_expr(&mut tokens_stream.peekable(), self, scope) } @@ -628,8 +625,8 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` - pub fn eval(&self, input: &str) -> Result { - self.eval_with_scope(&mut Scope::new(), input) + pub fn eval(&self, script: &str) -> Result { + self.eval_with_scope(&mut Scope::new(), script) } /// Evaluate a string with own scope. @@ -657,9 +654,9 @@ impl<'e> Engine<'e> { pub fn eval_with_scope( &self, scope: &mut Scope, - input: &str, + script: &str, ) -> Result { - let ast = self.compile(input).map_err(EvalAltResult::ErrorParsing)?; + let ast = self.compile(script)?; self.eval_ast_with_scope(scope, &ast) } @@ -677,8 +674,8 @@ impl<'e> Engine<'e> { /// # Ok(()) /// # } /// ``` - pub fn eval_expression(&self, input: &str) -> Result { - self.eval_expression_with_scope(&mut Scope::new(), input) + pub fn eval_expression(&self, script: &str) -> Result { + self.eval_expression_with_scope(&mut Scope::new(), script) } /// Evaluate a string containing an expression with own scope. @@ -702,11 +699,9 @@ impl<'e> Engine<'e> { pub fn eval_expression_with_scope( &self, scope: &mut Scope, - input: &str, + script: &str, ) -> Result { - let ast = self - .compile_expression(input) - .map_err(EvalAltResult::ErrorParsing)?; + let ast = self.compile_expression(script)?; self.eval_ast_with_scope(scope, &ast) } @@ -813,14 +808,14 @@ impl<'e> Engine<'e> { /// Evaluate a string, 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. - pub fn consume(&self, input: &str) -> Result<(), EvalAltResult> { - self.consume_with_scope(&mut Scope::new(), input) + pub fn consume(&self, script: &str) -> Result<(), EvalAltResult> { + self.consume_with_scope(&mut Scope::new(), script) } /// 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. - pub fn consume_with_scope(&self, scope: &mut Scope, input: &str) -> Result<(), EvalAltResult> { - let tokens_stream = lex(input); + pub fn consume_with_scope(&self, scope: &mut Scope, script: &str) -> Result<(), EvalAltResult> { + let tokens_stream = lex(script); let ast = parse( &mut tokens_stream.peekable(), @@ -828,8 +823,7 @@ impl<'e> Engine<'e> { scope, #[cfg(not(feature = "no_optimize"))] self.optimization_level, - ) - .map_err(EvalAltResult::ErrorParsing)?; + )?; self.consume_ast_with_scope(scope, &ast) } From 518725e119a2534dd0290e10a1cc539052b10152 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Apr 2020 23:01:48 +0800 Subject: [PATCH 09/21] Add support for anonymous functions in Rust. --- README.md | 45 +++++++++++++- src/api.rs | 2 +- src/fn_anonymous.rs | 118 ++++++++++++++++++++++++++++++++++++ src/{call.rs => fn_call.rs} | 0 src/fn_register.rs | 4 -- src/lib.rs | 8 ++- tests/call_fn.rs | 15 ++++- 7 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 src/fn_anonymous.rs rename src/{call.rs => fn_call.rs} (100%) diff --git a/README.md b/README.md index 90609c5a..31fe5dc2 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,10 @@ fn main() -> Result<(), EvalAltResult> } ``` +`EvalAltResult` is a Rust `enum` containing all errors encountered during the parsing or evaluation process. + +### Script evaluation + 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. @@ -192,6 +196,8 @@ Evaluate a script file directly: let result = engine.eval_file::("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf' ``` +### Compiling scripts (to AST) + To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form: ```rust @@ -211,6 +217,8 @@ Compiling a script file is also supported: let ast = engine.compile_file("hello_world.rhai".into())?; ``` +### Calling Rhai functions from Rust + Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`. ```rust @@ -251,6 +259,37 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )? // ^^ unit = tuple of zero ``` +### Creating Rust anonymous functions from Rhai script + +[`AnonymousFn`]: #creating-rust-anonymous-functions-from-rhai-script + +It is possible to further encapsulate a script in Rust such that it essentially becomes a normal Rust function. +This is accomplished via the `AnonymousFn` trait which contains `create_from_script` (as well as its associate +method `create_from_ast`): + +```rust +use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_script' + +let engine = Engine::new(); // create a new 'Engine' just for this + +let script = "fn calc(x, y) { x + y.len() < 42 }"; + +// AnonymousFn takes two type parameters: +// 1) a tuple made up of the types of the script function's parameters +// 2) the return type of the script function +// +// 'func' will have type Box Result> and is callable! +let func = AnonymousFn::<(i64, String), bool>::create_from_script( +// ^^^^^^^^^^^^^ function parameter types in tuple + + engine, // the 'Engine' is consumed into the closure + script, // the script, notice number of parameters must match + "calc" // the entry-point function name +)?; + +func(123, "hello".to_string())? == false; // call the anonymous function +``` + Raw `Engine` ------------ @@ -448,8 +487,8 @@ To call these functions, they need to be registered with the [`Engine`]. ```rust use rhai::{Engine, EvalAltResult}; -use rhai::RegisterFn; // use `RegisterFn` trait for `register_fn` -use rhai::{Any, Dynamic, RegisterDynamicFn}; // use `RegisterDynamicFn` trait for `register_dynamic_fn` +use rhai::RegisterFn; // use 'RegisterFn' trait for 'register_fn' +use rhai::{Any, Dynamic, RegisterDynamicFn}; // use 'RegisterDynamicFn' trait for 'register_dynamic_fn' // Normal function fn add(x: i64, y: i64) -> i64 { @@ -536,7 +575,7 @@ and the error text gets converted into `EvalAltResult::ErrorRuntime`. ```rust use rhai::{Engine, EvalAltResult, Position}; -use rhai::RegisterResultFn; // use `RegisterResultFn` trait for `register_result_fn` +use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' // Function that may fail fn safe_divide(x: i64, y: i64) -> Result { diff --git a/src/api.rs b/src/api.rs index a33526d2..e0e53ba4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,9 +1,9 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Any, AnyExt, Dynamic}; -use crate::call::FuncArgs; use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec}; use crate::error::ParseError; +use crate::fn_call::FuncArgs; use crate::fn_register::RegisterFn; use crate::parser::{lex, parse, parse_global_expr, Position, AST}; use crate::result::EvalAltResult; diff --git a/src/fn_anonymous.rs b/src/fn_anonymous.rs new file mode 100644 index 00000000..ecdfff25 --- /dev/null +++ b/src/fn_anonymous.rs @@ -0,0 +1,118 @@ +//! Module which defines the function registration mechanism. +#![cfg(not(feature = "no_function"))] +#![allow(non_snake_case)] + +use crate::any::Any; +use crate::engine::Engine; +use crate::error::ParseError; +use crate::parser::AST; +use crate::result::EvalAltResult; +use crate::scope::Scope; + +/// A trait to create a Rust anonymous function from a script. +pub trait AnonymousFn { + type Output; + + /// Create a Rust anonymous function from an `AST`. + /// The `Engine` and `AST` are consumed and basically embedded into the closure. + /// + /// # Examples + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_ast' + /// + /// let engine = Engine::new(); // create a new 'Engine' just for this + /// + /// let ast = engine.compile("fn calc(x, y) { x + y.len() < 42 }")?; + /// + /// // AnonymousFn takes two type parameters: + /// // 1) a tuple made up of the types of the script function's parameters + /// // 2) the return type of the script function + /// // + /// // 'func' will have type Box Result> and is callable! + /// let func = AnonymousFn::<(i64, String), bool>::create_from_ast( + /// // ^^^^^^^^^^^^^ function parameter types in tuple + /// + /// engine, // the 'Engine' is consumed into the closure + /// ast, // the 'AST' + /// "calc" // the entry-point function name + /// ); + /// + /// func(123, "hello".to_string())? == false; // call the anonymous function + /// # Ok(()) + /// # } + fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output; + + /// Create a Rust anonymous function from a script. + /// The `Engine` is consumed and basically embedded into the closure. + /// + /// # Examples + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_script' + /// + /// let engine = Engine::new(); // create a new 'Engine' just for this + /// + /// let script = "fn calc(x, y) { x + y.len() < 42 }"; + /// + /// // AnonymousFn takes two type parameters: + /// // 1) a tuple made up of the types of the script function's parameters + /// // 2) the return type of the script function + /// // + /// // 'func' will have type Box Result> and is callable! + /// let func = AnonymousFn::<(i64, String), bool>::create_from_script( + /// // ^^^^^^^^^^^^^ function parameter types in tuple + /// + /// engine, // the 'Engine' is consumed into the closure + /// script, // the script, notice number of parameters must match + /// "calc" // the entry-point function name + /// )?; + /// + /// func(123, "hello".to_string())? == false; // call the anonymous function + /// # Ok(()) + /// # } + /// ``` + fn create_from_script( + self, + script: &str, + entry_point: &str, + ) -> Result; +} + +macro_rules! def_anonymous_fn { + () => { + def_anonymous_fn!(imp); + }; + (imp $($par:ident),*) => { + impl<'e, $($par: Any + Clone,)* RET: Any + Clone> AnonymousFn<($($par,)*), RET> for Engine<'e> + { + #[cfg(feature = "sync")] + type Output = Box Result + Send + Sync + 'e>; + + #[cfg(not(feature = "sync"))] + type Output = Box Result + 'e>; + + fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output { + let name = entry_point.to_string(); + + Box::new(move |$($par: $par),*| { + self.call_fn::<_, RET>(&mut Scope::new(), &ast, &name, ($($par,)*)) + }) + } + + fn create_from_script(self, script: &str, entry_point: &str) -> Result { + let ast = self.compile(script)?; + Ok(AnonymousFn::<($($par,)*), RET>::create_from_ast(self, ast, entry_point)) + } + } + }; + ($p0:ident $(, $p:ident)*) => { + def_anonymous_fn!(imp $p0 $(, $p)*); + def_anonymous_fn!($($p),*); + }; +} + +#[rustfmt::skip] +def_anonymous_fn!(A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V); diff --git a/src/call.rs b/src/fn_call.rs similarity index 100% rename from src/call.rs rename to src/fn_call.rs diff --git a/src/fn_register.rs b/src/fn_register.rs index 7ff2cc28..f01ffa0c 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -262,10 +262,6 @@ macro_rules! def_register { def_register!($($p),*); }; -// (imp_pop) => {}; -// (imp_pop $head:ident => $head_mark:ty => $head_param:ty $(,$tail:ident => $tail_mark:ty => $tp:ty)*) => { -// def_register!(imp $($tail => $tail_mark => $tp),*); -// }; } #[rustfmt::skip] diff --git a/src/lib.rs b/src/lib.rs index b2982f08..e61ad90a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,9 +61,10 @@ extern crate alloc; mod any; mod api; mod builtin; -mod call; mod engine; mod error; +mod fn_anonymous; +mod fn_call; mod fn_register; mod optimize; mod parser; @@ -72,14 +73,17 @@ mod scope; mod stdlib; pub use any::{Any, AnyExt, Dynamic, Variant}; -pub use call::FuncArgs; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; +pub use fn_call::FuncArgs; pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; pub use parser::{Position, AST, INT}; pub use result::EvalAltResult; pub use scope::Scope; +#[cfg(not(feature = "no_function"))] +pub use fn_anonymous::AnonymousFn; + #[cfg(not(feature = "no_index"))] pub use engine::Array; diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 4ee2c3a6..71df9489 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,5 +1,5 @@ #![cfg(not(feature = "no_function"))] -use rhai::{Engine, EvalAltResult, ParseErrorType, Scope, INT}; +use rhai::{AnonymousFn, Engine, EvalAltResult, ParseErrorType, Scope, INT}; #[test] fn test_fn() -> Result<(), EvalAltResult> { @@ -59,3 +59,16 @@ fn test_call_fn() -> Result<(), EvalAltResult> { Ok(()) } + +#[test] +fn test_anonymous_fn() -> Result<(), EvalAltResult> { + let calc_func = AnonymousFn::<(INT, INT, INT), INT>::create_from_script( + Engine::new(), + "fn calc(x, y, z) { (x + y) * z }", + "calc", + )?; + + assert_eq!(calc_func(42, 123, 9)?, 1485); + + Ok(()) +} From 34ef2d6e0094585168ae1e457e9a27423fd81dbd Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 9 Apr 2020 10:38:33 +0800 Subject: [PATCH 10/21] Rename AnonymousFn to Func --- README.md | 35 +++++++++++++++++++++-------- src/{fn_anonymous.rs => fn_func.rs} | 26 +++++++++++---------- src/lib.rs | 22 +++++++++++++----- tests/call_fn.rs | 7 ++++-- 4 files changed, 61 insertions(+), 29 deletions(-) rename src/{fn_anonymous.rs => fn_func.rs} (80%) diff --git a/README.md b/README.md index 31fe5dc2..2d801179 100644 --- a/README.md +++ b/README.md @@ -261,26 +261,26 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )? ### Creating Rust anonymous functions from Rhai script -[`AnonymousFn`]: #creating-rust-anonymous-functions-from-rhai-script +[`Func`]: #creating-rust-anonymous-functions-from-rhai-script It is possible to further encapsulate a script in Rust such that it essentially becomes a normal Rust function. -This is accomplished via the `AnonymousFn` trait which contains `create_from_script` (as well as its associate +This is accomplished via the `Func` trait which contains `create_from_script` (as well as its associate method `create_from_ast`): ```rust -use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_script' +use rhai::{Engine, Func}; // use 'Func' for 'create_from_script' let engine = Engine::new(); // create a new 'Engine' just for this let script = "fn calc(x, y) { x + y.len() < 42 }"; -// AnonymousFn takes two type parameters: +// Func takes two type parameters: // 1) a tuple made up of the types of the script function's parameters // 2) the return type of the script function // // 'func' will have type Box Result> and is callable! -let func = AnonymousFn::<(i64, String), bool>::create_from_script( -// ^^^^^^^^^^^^^ function parameter types in tuple +let func = Func::<(i64, String), bool>::create_from_script( +// ^^^^^^^^^^^^^ function parameter types in tuple engine, // the 'Engine' is consumed into the closure script, // the script, notice number of parameters must match @@ -336,6 +336,7 @@ Values and types [`type_of()`]: #values-and-types [`to_string()`]: #values-and-types +[`()`]: #values-and-types The following primitive types are supported natively: @@ -353,8 +354,6 @@ The following primitive types are supported natively: | **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | | **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` | `""` _(empty string)_ | -[`()`]: #values-and-types - 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. @@ -479,6 +478,20 @@ let c = 'X'; // character print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88" ``` +Traits +------ + +A number of traits, under the `rhai::` module namespace, provide additional functionalities. + +| Trait | Description | Methods | +| ------------------- | --------------------------------------------------------------------------------- | --------------------------------------- | +| `Any` | Generic trait that represents a [`Dynamic`] type | `type_id`, `type_name`, `into_dynamic` | +| `AnyExt` | Extension trait to allows casting of a [`Dynamic`] value to Rust types | `cast`, `try_cast` | +| `RegisterFn` | Trait for registering functions | `register_fn` | +| `RegisterDynamicFn` | Trait for registering functions returning [`Dynamic`] | `register_dynamic_fn` | +| `RegisterResultFn` | Trait for registering fallible functions returning `Result<`_T_`, EvalAltResult>` | `register_result_fn` | +| `Func` | Trait for creating anonymous functions from script | `create_from_ast`, `create_from_script` | + Working with functions ---------------------- @@ -1245,7 +1258,7 @@ Property values can be accessed via the dot notation (_object_ `.` _property_) o The dot notation allows only property names that follow the same naming rules as [variables]. The index notation allows setting/getting properties of arbitrary names (even the empty string). -**Important:** Trying to read a non-existent property returns `()` instead of causing an error. +**Important:** Trying to read a non-existent property returns [`()`] instead of causing an error. The Rust type of a Rhai object map is `rhai::Map`. [`type_of()`] an object map returns `"map"`. @@ -1620,6 +1633,10 @@ fn do_addition(x) { } ``` +Unlike C/C++, functions can be defined _anywhere_ within the global level. A function does not need to be defined +prior to being used in a script; a statement in the script can freely call a function defined afterwards. +This is similar to Rust and many other modern languages. + ### Functions overloading Functions can be _overloaded_ and are resolved purely upon the function's _name_ and the _number_ of parameters diff --git a/src/fn_anonymous.rs b/src/fn_func.rs similarity index 80% rename from src/fn_anonymous.rs rename to src/fn_func.rs index ecdfff25..7350d6d5 100644 --- a/src/fn_anonymous.rs +++ b/src/fn_func.rs @@ -9,8 +9,10 @@ use crate::parser::AST; use crate::result::EvalAltResult; use crate::scope::Scope; +use crate::stdlib::{boxed::Box, string::ToString}; + /// A trait to create a Rust anonymous function from a script. -pub trait AnonymousFn { +pub trait Func { type Output; /// Create a Rust anonymous function from an `AST`. @@ -20,19 +22,19 @@ pub trait AnonymousFn { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { - /// use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_ast' + /// use rhai::{Engine, Func}; // use 'Func' for 'create_from_ast' /// /// let engine = Engine::new(); // create a new 'Engine' just for this /// /// let ast = engine.compile("fn calc(x, y) { x + y.len() < 42 }")?; /// - /// // AnonymousFn takes two type parameters: + /// // Func takes two type parameters: /// // 1) a tuple made up of the types of the script function's parameters /// // 2) the return type of the script function /// // /// // 'func' will have type Box Result> and is callable! - /// let func = AnonymousFn::<(i64, String), bool>::create_from_ast( - /// // ^^^^^^^^^^^^^ function parameter types in tuple + /// let func = Func::<(i64, String), bool>::create_from_ast( + /// // ^^^^^^^^^^^^^ function parameter types in tuple /// /// engine, // the 'Engine' is consumed into the closure /// ast, // the 'AST' @@ -51,19 +53,19 @@ pub trait AnonymousFn { /// /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { - /// use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_script' + /// use rhai::{Engine, Func}; // use 'Func' for 'create_from_script' /// /// let engine = Engine::new(); // create a new 'Engine' just for this /// /// let script = "fn calc(x, y) { x + y.len() < 42 }"; /// - /// // AnonymousFn takes two type parameters: + /// // Func takes two type parameters: /// // 1) a tuple made up of the types of the script function's parameters /// // 2) the return type of the script function /// // /// // 'func' will have type Box Result> and is callable! - /// let func = AnonymousFn::<(i64, String), bool>::create_from_script( - /// // ^^^^^^^^^^^^^ function parameter types in tuple + /// let func = Func::<(i64, String), bool>::create_from_script( + /// // ^^^^^^^^^^^^^ function parameter types in tuple /// /// engine, // the 'Engine' is consumed into the closure /// script, // the script, notice number of parameters must match @@ -86,7 +88,7 @@ macro_rules! def_anonymous_fn { def_anonymous_fn!(imp); }; (imp $($par:ident),*) => { - impl<'e, $($par: Any + Clone,)* RET: Any + Clone> AnonymousFn<($($par,)*), RET> for Engine<'e> + impl<'e, $($par: Any + Clone,)* RET: Any + Clone> Func<($($par,)*), RET> for Engine<'e> { #[cfg(feature = "sync")] type Output = Box Result + Send + Sync + 'e>; @@ -98,13 +100,13 @@ macro_rules! def_anonymous_fn { let name = entry_point.to_string(); Box::new(move |$($par: $par),*| { - self.call_fn::<_, RET>(&mut Scope::new(), &ast, &name, ($($par,)*)) + self.call_fn(&mut Scope::new(), &ast, &name, ($($par,)*)) }) } fn create_from_script(self, script: &str, entry_point: &str) -> Result { let ast = self.compile(script)?; - Ok(AnonymousFn::<($($par,)*), RET>::create_from_ast(self, ast, entry_point)) + Ok(Func::<($($par,)*), RET>::create_from_ast(self, ast, entry_point)) } } }; diff --git a/src/lib.rs b/src/lib.rs index e61ad90a..47832e87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,15 +3,19 @@ //! Rhai is a tiny, simple and very fast embedded scripting language for Rust //! that gives you a safe and easy way to add scripting to your applications. //! It provides a familiar syntax based on JS and Rust and a simple Rust interface. -//! Here is a quick example. First, the contents of `my_script.rhai`: +//! Here is a quick example. +//! +//! First, the contents of `my_script.rhai`: //! //! ```,ignore +//! // Brute force factorial function //! fn factorial(x) { //! if x == 1 { return 1; } //! x * factorial(x - 1) //! } //! -//! compute_something(factorial(10)) +//! // Calling an external function 'compute' +//! compute(factorial(10)) //! ``` //! //! And the Rust part: @@ -21,16 +25,22 @@ //! //! fn main() -> Result<(), EvalAltResult> //! { +//! // Define external function //! fn compute_something(x: i64) -> bool { //! (x % 40) == 0 //! } //! +//! // Create scripting engine //! let mut engine = Engine::new(); //! -//! engine.register_fn("compute_something", compute_something); +//! // Register external function as 'compute' +//! engine.register_fn("compute", compute_something); //! //! # #[cfg(not(feature = "no_std"))] -//! assert_eq!(engine.eval_file::("my_script.rhai".into())?, true); +//! assert_eq!( +//! engine.eval_file::("my_script.rhai".into())?, +//! true +//! ); //! //! Ok(()) //! } @@ -63,8 +73,8 @@ mod api; mod builtin; mod engine; mod error; -mod fn_anonymous; mod fn_call; +mod fn_func; mod fn_register; mod optimize; mod parser; @@ -82,7 +92,7 @@ pub use result::EvalAltResult; pub use scope::Scope; #[cfg(not(feature = "no_function"))] -pub use fn_anonymous::AnonymousFn; +pub use fn_func::Func; #[cfg(not(feature = "no_index"))] pub use engine::Array; diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 71df9489..82f42f93 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,5 +1,5 @@ #![cfg(not(feature = "no_function"))] -use rhai::{AnonymousFn, Engine, EvalAltResult, ParseErrorType, Scope, INT}; +use rhai::{Engine, EvalAltResult, Func, ParseErrorType, Scope, INT}; #[test] fn test_fn() -> Result<(), EvalAltResult> { @@ -62,7 +62,10 @@ fn test_call_fn() -> Result<(), EvalAltResult> { #[test] fn test_anonymous_fn() -> Result<(), EvalAltResult> { - let calc_func = AnonymousFn::<(INT, INT, INT), INT>::create_from_script( + let calc_func: Box Result> = + Engine::new().create_from_script("fn calc() { 42 }", "calc")?; + + let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), "fn calc(x, y, z) { (x + y) * z }", "calc", From 3609150dcfe49fff5fd96564687e6aa98bf03aee Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 9 Apr 2020 18:45:49 +0800 Subject: [PATCH 11/21] Bump version to 0.12.0. --- Cargo.toml | 2 +- README.md | 22 +++++++++++++------ src/engine.rs | 35 ++---------------------------- src/fn_register.rs | 10 +++++---- src/lib.rs | 1 + src/optimize.rs | 53 ++++++++++++++++++++++++++++++++++------------ tests/call_fn.rs | 3 --- tests/stack.rs | 4 ++-- 8 files changed, 67 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 632018f6..29511043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.11.0" +version = "0.12.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/README.md b/README.md index 2d801179..e31fc25f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Rhai's current features set: to do checked arithmetic operations); for [`no_std`] builds, a number of additional dependencies are pulled in to provide for functionalities that used to be in `std`. -**Note:** Currently, the version is 0.11.0, so the language and API's may change before they stabilize. +**Note:** Currently, the version is 0.12.0, so the language and API's may change before they stabilize. Installation ------------ @@ -36,7 +36,7 @@ Install the Rhai crate by adding this line to `dependencies`: ```toml [dependencies] -rhai = "0.11.0" +rhai = "0.12.0" ``` Use the latest released crate version on [`crates.io`](https::/crates.io/crates/rhai/): @@ -207,7 +207,7 @@ let ast = engine.compile("40 + 2")?; for _ in 0..42 { let result: i64 = engine.eval_ast(&ast)?; -println!("Answer #{}: {}", i, result); // prints 42 + println!("Answer #{}: {}", i, result); // prints 42 } ``` @@ -263,9 +263,10 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )? [`Func`]: #creating-rust-anonymous-functions-from-rhai-script -It is possible to further encapsulate a script in Rust such that it essentially becomes a normal Rust function. -This is accomplished via the `Func` trait which contains `create_from_script` (as well as its associate -method `create_from_ast`): +It is possible to further encapsulate a script in Rust such that it becomes a normal Rust function. +Such an _anonymous function_ is basically a boxed closure, very useful as call-back functions. +Creating them is accomplished via the `Func` trait which contains `create_from_script` +(as well as its companion method `create_from_ast`): ```rust use rhai::{Engine, Func}; // use 'Func' for 'create_from_script' @@ -288,6 +289,15 @@ let func = Func::<(i64, String), bool>::create_from_script( )?; func(123, "hello".to_string())? == false; // call the anonymous function + +schedule_callback(func); // pass it as a callback to another function + +// Although there is nothing you can't do by manually writing out the closure yourself... +let engine = Engine::new(); +let ast = engine.compile(script)?; +schedule_callback(Box::new(move |x: i64, y: String| -> Result { + engine.call_fn(&mut Scope::new(), &ast, "calc", (x, y)) +})); ``` Raw `Engine` diff --git a/src/engine.rs b/src/engine.rs index f351f72e..559a6ef0 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -50,7 +50,7 @@ type IteratorFn = dyn Fn(&Dynamic) -> Box> + Send + type IteratorFn = dyn Fn(&Dynamic) -> Box>; #[cfg(debug_assertions)] -pub const MAX_CALL_STACK_DEPTH: usize = 42; +pub const MAX_CALL_STACK_DEPTH: usize = 32; #[cfg(not(debug_assertions))] pub const MAX_CALL_STACK_DEPTH: usize = 256; @@ -277,7 +277,7 @@ pub struct Engine<'e> { /// Maximum levels of call-stack to prevent infinite recursion. /// - /// Defaults to 42 for debug builds and 256 for non-debug builds. + /// Defaults to 32 for debug builds and 256 for non-debug builds. pub(crate) max_call_stack_depth: usize, } @@ -372,11 +372,6 @@ impl Engine<'_> { /// Create a new `Engine` pub fn new() -> Self { - // fn abc(f: F) { - // f(); - // } - // abc(|| ()); - Default::default() } @@ -420,32 +415,6 @@ impl Engine<'_> { self.max_call_stack_depth = levels } - /// Call a registered function - #[cfg(not(feature = "no_optimize"))] - pub(crate) fn call_ext_fn_raw( - &self, - fn_name: &str, - args: &mut FnCallArgs, - pos: Position, - ) -> Result, EvalAltResult> { - let spec = FnSpec { - name: fn_name.into(), - args: args.iter().map(|a| Any::type_id(*a)).collect(), - }; - - // Search built-in's and external functions - if let Some(functions) = &self.functions { - if let Some(func) = functions.get(&spec) { - // Run external function - Ok(Some(func(args, pos)?)) - } else { - Ok(None) - } - } else { - Ok(None) - } - } - /// Universal method for calling functions either registered with the `Engine` or written in Rhai pub(crate) fn call_fn_raw( &self, diff --git a/src/fn_register.rs b/src/fn_register.rs index f01ffa0c..9030066a 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -132,9 +132,9 @@ macro_rules! def_register { def_register!(imp); }; (imp $($par:ident => $mark:ty => $param:ty => $clone:expr),*) => { - // ^ function parameter generic type name + // ^ function parameter generic type name (A, B, C etc.) // ^ function parameter marker type (T, Ref or Mut) - // ^ function parameter actual type + // ^ function parameter actual type (T, &T or &mut T) // ^ dereferencing function impl< $($par: Any + Clone,)* @@ -171,6 +171,7 @@ macro_rules! def_register { let r = f($(($clone)($par)),*); Ok(Box::new(r) as Dynamic) }; + self.register_fn_raw(name, vec![$(TypeId::of::<$par>()),*], Box::new(func)); } } @@ -255,9 +256,10 @@ macro_rules! def_register { def_register!(imp $p0 => $p0 => $p0 => Clone::clone $(, $p => $p => $p => Clone::clone)*); def_register!(imp $p0 => Mut<$p0> => &mut $p0 => identity $(, $p => $p => $p => Clone::clone)*); // handle the first parameter ^ first parameter passed through - // others passed by value (cloned) ^ + // ^ others passed by value (cloned) - // No support for functions where the first argument is a reference + // Currently does not support first argument which is a reference, as there will be + // conflicting implementations since &T: Any and T: Any cannot be distinguished //def_register!(imp $p0 => Ref<$p0> => &$p0 => identity $(, $p => $p => $p => Clone::clone)*); def_register!($($p),*); diff --git a/src/lib.rs b/src/lib.rs index 47832e87..dd4155ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ //! //! # #[cfg(not(feature = "no_std"))] //! assert_eq!( +//! // Evaluate the script, expects a 'bool' return //! engine.eval_file::("my_script.rhai".into())?, //! true //! ); diff --git a/src/optimize.rs b/src/optimize.rs index 74b77395..86950423 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -2,14 +2,16 @@ use crate::any::{Any, Dynamic}; use crate::engine::{ - Engine, FunctionsLib, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_EVAL, KEYWORD_PRINT, - KEYWORD_TYPE_OF, + Engine, FnAny, FnCallArgs, FnSpec, FunctionsLib, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_EVAL, + KEYWORD_PRINT, KEYWORD_TYPE_OF, }; -use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Position, ReturnType, Stmt, AST}; +use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::stdlib::{ boxed::Box, + collections::HashMap, rc::Rc, string::{String, ToString}, sync::Arc, @@ -96,6 +98,25 @@ impl<'a> State<'a> { } } +/// Call a registered function +fn call_fn( + functions: Option<&HashMap>>, + fn_name: &str, + args: &mut FnCallArgs, + pos: Position, +) -> Result, EvalAltResult> { + let spec = FnSpec { + name: fn_name.into(), + args: args.iter().map(|a| Any::type_id(*a)).collect(), + }; + + // Search built-in's and external functions + functions + .and_then(|f| f.get(&spec)) + .map(|func| func(args, pos)) + .transpose() +} + /// Optimize a statement. fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt { match stmt { @@ -528,21 +549,25 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { "" }; - state.engine.call_ext_fn_raw(&id, &mut call_args, pos).ok().map(|result| - result.or_else(|| { - if !arg_for_type_of.is_empty() { - // Handle `type_of()` - Some(arg_for_type_of.to_string().into_dynamic()) - } else { - // Otherwise use the default value, if any - def_value.clone() - } - }).and_then(|result| map_dynamic_to_expr(result, pos)) + call_fn(state.engine.functions.as_ref(), &id, &mut call_args, pos).ok() + .and_then(|result| + result.or_else(|| { + if !arg_for_type_of.is_empty() { + // Handle `type_of()` + Some(arg_for_type_of.to_string().into_dynamic()) + } else { + // Otherwise use the default value, if any + def_value.clone() + } + }).and_then(|result| map_dynamic_to_expr(result, pos)) .map(|expr| { state.set_dirty(); expr }) - ).flatten().unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) + ).unwrap_or_else(|| + // Optimize function call arguments + Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos) + ) } // id(args ..) -> optimize function call arguments diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 82f42f93..fc658a15 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -62,9 +62,6 @@ fn test_call_fn() -> Result<(), EvalAltResult> { #[test] fn test_anonymous_fn() -> Result<(), EvalAltResult> { - let calc_func: Box Result> = - Engine::new().create_from_script("fn calc() { 42 }", "calc")?; - let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( Engine::new(), "fn calc(x, y, z) { (x + y) * z }", diff --git a/tests/stack.rs b/tests/stack.rs index feee09d5..79a16221 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -9,10 +9,10 @@ fn test_stack_overflow() -> Result<(), EvalAltResult> { engine.eval::( r" fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } } - foo(38) + foo(30) ", )?, - 741 + 465 ); match engine.eval::<()>( From adbfceb5beb134ebc21f40d0857ad477cc6cf174 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 12:16:39 +0800 Subject: [PATCH 12/21] Do not omit error enum variants. --- README.md | 37 +++--- src/api.rs | 15 +-- src/engine.rs | 333 ++++++++++++++++++++---------------------------- src/error.rs | 41 +++--- src/optimize.rs | 28 ++-- src/parser.rs | 201 +++++++++++------------------ src/result.rs | 2 +- 7 files changed, 271 insertions(+), 386 deletions(-) diff --git a/README.md b/README.md index e31fc25f..9393fd1b 100644 --- a/README.md +++ b/README.md @@ -314,9 +314,9 @@ Use `Engine::new_raw` to create a _raw_ `Engine`, in which: * the _standard library_ of utility functions is _not_ loaded by default (load it using the `register_stdlib` method). ```rust -let mut engine = Engine::new_raw(); // Create a 'raw' Engine +let mut engine = Engine::new_raw(); // create a 'raw' Engine -engine.register_stdlib(); // Register the standard library manually +engine.register_stdlib(); // register the standard library manually engine. ``` @@ -439,7 +439,7 @@ The `cast` method (from the `rhai::AnyExt` trait) then converts the value into a Alternatively, use the `try_cast` method which does not panic but returns an error when the cast fails. ```rust -use rhai::AnyExt; // Pull in the trait. +use rhai::AnyExt; // pull in the trait. let list: Array = engine.eval("...")?; // return type is 'Array' let item = list[0]; // an element in an 'Array' is 'Dynamic' @@ -455,14 +455,14 @@ let value = item.try_cast::()?; // 'try_cast' does not panic whe The `type_name` method gets the name of the actual type as a static string slice, which you may match against. ```rust -use rhai::Any; // Pull in the trait. +use rhai::Any; // pull in the trait. let list: Array = engine.eval("...")?; // return type is 'Array' let item = list[0]; // an element in an 'Array' is 'Dynamic' match item.type_name() { // 'type_name' returns the name of the actual Rust type "i64" => ... - "std::string::String" => ... + "alloc::string::String" => ... "bool" => ... "path::to::module::TestStruct" => ... } @@ -548,7 +548,7 @@ To return a [`Dynamic`] value from a Rust function, use the `into_dynamic()` met (under the `rhai::Any` trait) to convert it. ```rust -use rhai::Any; // Pull in the trait +use rhai::Any; // pull in the trait fn decide(yes_no: bool) -> Dynamic { if yes_no { @@ -598,13 +598,13 @@ and the error text gets converted into `EvalAltResult::ErrorRuntime`. ```rust use rhai::{Engine, EvalAltResult, Position}; -use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' +use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn' // Function that may fail fn safe_divide(x: i64, y: i64) -> Result { if y == 0 { // Return an error if y is zero - Err("Division by zero detected!".into()) // short-cut to create EvalAltResult + Err("Division by zero!".into()) // short-cut to create EvalAltResult } else { Ok(x / y) } @@ -618,7 +618,7 @@ fn main() engine.register_result_fn("divide", safe_divide); if let Err(error) = engine.eval::("divide(40, 0)") { - println!("Error: {:?}", error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") + println!("Error: {:?}", error); // prints ErrorRuntime("Division by zero detected!", (1, 1)") } } ``` @@ -672,7 +672,7 @@ fn main() -> Result<(), EvalAltResult> let result = engine.eval::("let x = new_ts(); x.update(); x")?; - println!("result: {}", result.field); // prints 42 + println!("result: {}", result.field); // prints 42 Ok(()) } @@ -1329,7 +1329,7 @@ foo == 42; y.has("a") == true; y.has("xyz") == false; -y.xyz == (); // A non-existing property returns '()' +y.xyz == (); // a non-existing property returns '()' y["xyz"] == (); print(y.len()); // prints 3 @@ -1449,10 +1449,11 @@ if (decision) print("I've decided!"); Like Rust, `if` statements can also be used as _expressions_, replacing the `? :` conditional operators in other C-like languages. ```rust -let x = 1 + if true { 42 } else { 123 } / 2; +// The following is equivalent to C: int x = 1 + (decision ? 42 : 123) / 2; +let x = 1 + if decision { 42 } else { 123 } / 2; x == 22; -let x = if false { 42 }; // No else branch defaults to '()' +let x = if decision { 42 }; // no else branch defaults to '()' x == (); ``` @@ -1506,7 +1507,6 @@ for x in range(0, 50) { if x == 42 { break; } // break out of for loop } - // The 'range' function also takes a step for x in range(0, 50, 3) { // step by 3 if x > 10 { continue; } // skip to the next iteration @@ -1514,15 +1514,20 @@ for x in range(0, 50, 3) { // step by 3 if x == 42 { break; } // break out of for loop } -// Iterate through the values of an object map +// Iterate through object map let map = #{a:1, b:3, c:5, d:7, e:9}; -// Remember that keys are returned in random order +// Property names are returned in random order for x in keys(map) { if x > 10 { continue; } // skip to the next iteration print(x); if x == 42 { break; } // break out of for loop } + +// Property values are returned in random order +for val in values(map) { + print(val); +} ``` `return`-ing values diff --git a/src/api.rs b/src/api.rs index e0e53ba4..a2c7755a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -5,13 +5,11 @@ use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec}; use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_register::RegisterFn; +use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::parser::{lex, parse, parse_global_expr, Position, AST}; use crate::result::EvalAltResult; use crate::scope::Scope; -#[cfg(not(feature = "no_optimize"))] -use crate::optimize::{optimize_into_ast, OptimizationLevel}; - use crate::stdlib::{ any::{type_name, TypeId}, boxed::Box, @@ -387,12 +385,7 @@ impl<'e> Engine<'e> { /// # } /// ``` pub fn compile_with_scope(&self, scope: &Scope, script: &str) -> Result { - self.compile_with_scope_and_optimization_level( - scope, - script, - #[cfg(not(feature = "no_optimize"))] - self.optimization_level, - ) + self.compile_with_scope_and_optimization_level(scope, script, self.optimization_level) } /// Compile a string into an `AST` using own scope at a specific optimization level. @@ -400,7 +393,7 @@ impl<'e> Engine<'e> { &self, scope: &Scope, script: &str, - #[cfg(not(feature = "no_optimize"))] optimization_level: OptimizationLevel, + optimization_level: OptimizationLevel, ) -> Result { let tokens_stream = lex(script); @@ -408,7 +401,6 @@ impl<'e> Engine<'e> { &mut tokens_stream.peekable(), self, scope, - #[cfg(not(feature = "no_optimize"))] optimization_level, ) } @@ -821,7 +813,6 @@ impl<'e> Engine<'e> { &mut tokens_stream.peekable(), self, scope, - #[cfg(not(feature = "no_optimize"))] self.optimization_level, )?; diff --git a/src/engine.rs b/src/engine.rs index 559a6ef0..c308b236 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2,13 +2,11 @@ use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::error::ParseErrorType; +use crate::optimize::OptimizationLevel; use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt, INT}; use crate::result::EvalAltResult; use crate::scope::{EntryRef as ScopeSource, EntryType as ScopeEntryType, Scope}; -#[cfg(not(feature = "no_optimize"))] -use crate::optimize::OptimizationLevel; - use crate::stdlib::{ any::{type_name, TypeId}, borrow::Cow, @@ -28,13 +26,11 @@ use crate::stdlib::{ /// An dynamic array of `Dynamic` values. /// /// Not available under the `no_index` feature. -#[cfg(not(feature = "no_index"))] pub type Array = Vec; /// An dynamic hash map of `Dynamic` values with `String` keys. /// /// Not available under the `no_object` feature. -#[cfg(not(feature = "no_object"))] pub type Map = HashMap; pub type FnCallArgs<'a> = [&'a mut Variant]; @@ -57,7 +53,6 @@ pub const MAX_CALL_STACK_DEPTH: usize = 256; pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; -pub const KEYWORD_DUMP_AST: &str = "dump_ast"; pub const KEYWORD_TYPE_OF: &str = "type_of"; pub const KEYWORD_EVAL: &str = "eval"; pub const FUNC_TO_STRING: &str = "to_string"; @@ -65,12 +60,10 @@ pub const FUNC_GETTER: &str = "get$"; pub const FUNC_SETTER: &str = "set$"; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] -#[cfg(not(feature = "no_index"))] enum IndexSourceType { Expression, String, Array, - #[cfg(not(feature = "no_object"))] Map, } @@ -272,7 +265,6 @@ pub struct Engine<'e> { pub(crate) on_debug: Option>, /// Optimize the AST after compilation. - #[cfg(not(feature = "no_optimize"))] pub(crate) optimization_level: OptimizationLevel, /// Maximum levels of call-stack to prevent infinite recursion. @@ -290,18 +282,13 @@ impl Default for Engine<'_> { type_names: None, // default print/debug implementations - #[cfg(not(feature = "no_std"))] on_print: Some(Box::new(default_print)), - #[cfg(not(feature = "no_std"))] on_debug: Some(Box::new(default_print)), - // default print/debug implementations - #[cfg(feature = "no_std")] - on_print: None, - #[cfg(feature = "no_std")] - on_debug: None, - // optimization level + #[cfg(feature = "no_optimize")] + optimization_level: OptimizationLevel::None, + #[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "optimize_full"))] optimization_level: OptimizationLevel::Simple, @@ -330,9 +317,16 @@ pub fn make_getter(id: &str) -> String { /// Extract the property name from a getter function name. fn extract_prop_from_getter(fn_name: &str) -> Option<&str> { - if fn_name.starts_with(FUNC_GETTER) { - Some(&fn_name[FUNC_GETTER.len()..]) - } else { + #[cfg(not(feature = "no_object"))] + { + if fn_name.starts_with(FUNC_GETTER) { + Some(&fn_name[FUNC_GETTER.len()..]) + } else { + None + } + } + #[cfg(feature = "no_object")] + { None } } @@ -344,9 +338,16 @@ pub fn make_setter(id: &str) -> String { /// Extract the property name from a setter function name. fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { - if fn_name.starts_with(FUNC_SETTER) { - Some(&fn_name[FUNC_SETTER.len()..]) - } else { + #[cfg(not(feature = "no_object"))] + { + if fn_name.starts_with(FUNC_SETTER) { + Some(&fn_name[FUNC_SETTER.len()..]) + } else { + None + } + } + #[cfg(feature = "no_object")] + { None } } @@ -384,6 +385,9 @@ impl Engine<'_> { on_print: None, on_debug: None, + #[cfg(feature = "no_optimize")] + optimization_level: OptimizationLevel::None, + #[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "optimize_full"))] optimization_level: OptimizationLevel::Simple, @@ -431,6 +435,9 @@ impl Engine<'_> { return Err(EvalAltResult::ErrorStackOverflow(pos)); } + #[cfg(feature = "no_function")] + const fn_lib: Option<&FunctionsLib> = None; + // First search in script-defined functions (can override built-in) if let Some(lib) = fn_lib { if let Some(fn_def) = lib.get_function(fn_name, args.len()) { @@ -523,12 +530,9 @@ impl Engine<'_> { } if let Some(prop) = extract_prop_from_getter(fn_name) { - #[cfg(not(feature = "no_object"))] - { - // Map property access - if let Some(map) = args[0].downcast_ref::() { - return Ok(map.get(prop).cloned().unwrap_or_else(|| ().into_dynamic())); - } + // Map property access + if let Some(map) = args[0].downcast_ref::() { + return Ok(map.get(prop).cloned().unwrap_or_else(|| ().into_dynamic())); } // Getter function not found @@ -539,15 +543,12 @@ impl Engine<'_> { } if let Some(prop) = extract_prop_from_setter(fn_name) { - #[cfg(not(feature = "no_object"))] - { - let value = args[1].into_dynamic(); + let value = args[1].into_dynamic(); - // Map property update - if let Some(map) = args[0].downcast_mut::() { - map.insert(prop.to_string(), value); - return Ok(().into_dynamic()); - } + // Map property update + if let Some(map) = args[0].downcast_mut::() { + map.insert(prop.to_string(), value); + return Ok(().into_dynamic()); } // Setter function not found @@ -576,7 +577,6 @@ impl Engine<'_> { } /// Chain-evaluate a dot setter. - #[cfg(not(feature = "no_object"))] fn get_dot_val_helper( &self, scope: &mut Scope, @@ -611,7 +611,6 @@ impl Engine<'_> { } // xxx.idx_lhs[idx_expr] - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, idx_expr, op_pos) => { let value = match idx_lhs.as_ref() { // xxx.id[idx_expr] @@ -648,7 +647,6 @@ impl Engine<'_> { }) } // xxx.idx_lhs[idx_expr].rhs - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, idx_expr, op_pos) => { let val = match idx_lhs.as_ref() { // xxx.id[idx_expr].rhs @@ -692,7 +690,6 @@ impl Engine<'_> { } /// Evaluate a dot chain getter - #[cfg(not(feature = "no_object"))] fn get_dot_val( &self, scope: &mut Scope, @@ -715,9 +712,8 @@ impl Engine<'_> { } // idx_lhs[idx_expr].??? - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, idx_expr, op_pos) => { - let (idx_src_type, src, idx, mut val) = + let (idx_src_type, src, index, mut val) = self.eval_index_expr(scope, fn_lib, idx_lhs, idx_expr, *op_pos, level)?; let target = Target::from(val.as_mut()); let value = self.get_dot_val_helper(scope, fn_lib, target, dot_rhs, level); @@ -736,7 +732,7 @@ impl Engine<'_> { idx_src_type, scope, src, - idx, + index, (val, dot_rhs.position()), )?; } @@ -766,7 +762,6 @@ impl Engine<'_> { } /// Get the value at the indexed position of a base type - #[cfg(not(feature = "no_index"))] fn get_indexed_value( &self, scope: &mut Scope, @@ -780,62 +775,61 @@ impl Engine<'_> { // val_array[idx] if let Some(arr) = val.downcast_ref::() { - let idx = self + let index = self .eval_expr(scope, fn_lib, idx_expr, level)? .try_cast::() .map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?; - return if idx >= 0 { - arr.get(idx as usize) + return if index >= 0 { + arr.get(index as usize) .cloned() - .map(|v| (v, IndexSourceType::Array, IndexValue::from_num(idx))) - .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos)) + .map(|v| (v, IndexSourceType::Array, IndexValue::from_num(index))) + .ok_or_else(|| EvalAltResult::ErrorArrayBounds(arr.len(), index, idx_pos)) } else { - Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, idx_pos)) + Err(EvalAltResult::ErrorArrayBounds(arr.len(), index, idx_pos)) }; } - #[cfg(not(feature = "no_object"))] - { - // val_map[idx] - if let Some(map) = val.downcast_ref::() { - let idx = self - .eval_expr(scope, fn_lib, idx_expr, level)? - .try_cast::() - .map_err(|_| EvalAltResult::ErrorStringIndexExpr(idx_expr.position()))?; + // val_map[idx] + if let Some(map) = val.downcast_ref::() { + let index = self + .eval_expr(scope, fn_lib, idx_expr, level)? + .try_cast::() + .map_err(|_| EvalAltResult::ErrorStringIndexExpr(idx_expr.position()))?; - return Ok(( - map.get(&idx).cloned().unwrap_or_else(|| ().into_dynamic()), - IndexSourceType::Map, - IndexValue::from_str(idx), - )); - } + return Ok(( + map.get(&index) + .cloned() + .unwrap_or_else(|| ().into_dynamic()), + IndexSourceType::Map, + IndexValue::from_str(index), + )); } // val_string[idx] if let Some(s) = val.downcast_ref::() { - let idx = self + let index = self .eval_expr(scope, fn_lib, idx_expr, level)? .try_cast::() .map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?; - return if idx >= 0 { + return if index >= 0 { s.chars() - .nth(idx as usize) + .nth(index as usize) .map(|ch| { ( ch.into_dynamic(), IndexSourceType::String, - IndexValue::from_num(idx), + IndexValue::from_num(index), ) }) .ok_or_else(|| { - EvalAltResult::ErrorStringBounds(s.chars().count(), idx, idx_pos) + EvalAltResult::ErrorStringBounds(s.chars().count(), index, idx_pos) }) } else { Err(EvalAltResult::ErrorStringBounds( s.chars().count(), - idx, + index, idx_pos, )) }; @@ -849,7 +843,6 @@ impl Engine<'_> { } /// Evaluate an index expression - #[cfg(not(feature = "no_index"))] fn eval_index_expr<'a>( &self, scope: &mut Scope, @@ -879,7 +872,7 @@ impl Engine<'_> { val, ) = Self::search_scope(scope, &id, lhs.position())?; - let (val, idx_src_type, idx) = + let (val, idx_src_type, index) = self.get_indexed_value(scope, fn_lib, &val, idx_expr, op_pos, level)?; Ok(( @@ -889,7 +882,7 @@ impl Engine<'_> { typ: src_type, index: src_idx, }), - idx, + index, val, )) } @@ -899,13 +892,12 @@ impl Engine<'_> { let val = self.eval_expr(scope, fn_lib, expr, level)?; self.get_indexed_value(scope, fn_lib, &val, idx_expr, op_pos, level) - .map(|(val, _, idx)| (IndexSourceType::Expression, None, idx, val)) + .map(|(val, _, index)| (IndexSourceType::Expression, None, index, val)) } } } /// Replace a character at an index position in a mutable string - #[cfg(not(feature = "no_index"))] fn str_replace_char(s: &mut String, idx: usize, new_ch: char) { let mut chars: Vec = s.chars().collect(); let ch = *chars.get(idx).expect("string index out of bounds"); @@ -919,7 +911,6 @@ impl Engine<'_> { } /// Update the value at an index position in a variable inside the scope - #[cfg(not(feature = "no_index"))] fn update_indexed_var_in_scope( idx_src_type: IndexSourceType, scope: &mut Scope, @@ -936,7 +927,6 @@ impl Engine<'_> { } // map_id[idx] = val - #[cfg(not(feature = "no_object"))] IndexSourceType::Map => { let arr = scope.get_mut_by_type::(src); arr.insert(idx.as_str(), new_val.0); @@ -961,7 +951,6 @@ impl Engine<'_> { } /// Update the value at an index position - #[cfg(not(feature = "no_index"))] fn update_indexed_value( mut target: Dynamic, idx: IndexValue, @@ -973,12 +962,9 @@ impl Engine<'_> { return Ok(target); } - #[cfg(not(feature = "no_object"))] - { - if let Some(map) = target.downcast_mut::() { - map.insert(idx.as_str(), new_val); - return Ok(target); - } + if let Some(map) = target.downcast_mut::() { + map.insert(idx.as_str(), new_val); + return Ok(target); } if let Some(s) = target.downcast_mut::() { @@ -995,7 +981,6 @@ impl Engine<'_> { } /// Chain-evaluate a dot setter - #[cfg(not(feature = "no_object"))] fn set_dot_val_helper( &self, scope: &mut Scope, @@ -1014,17 +999,16 @@ impl Engine<'_> { // xxx.lhs[idx_expr] // TODO - Allow chaining of indexing! - #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() { // xxx.id[idx_expr] Expr::Property(id, pos) => { let fn_name = make_getter(id); self.call_fn_raw(None, fn_lib, &fn_name, &mut [this_ptr], None, *pos, 0) .and_then(|val| { - let (_, _, idx) = self + let (_, _, index) = self .get_indexed_value(scope, fn_lib, &val, idx_expr, *op_pos, level)?; - Self::update_indexed_value(val, idx, new_val.0.clone(), new_val.1) + Self::update_indexed_value(val, index, new_val.0.clone(), new_val.1) }) .and_then(|mut val| { let fn_name = make_setter(id); @@ -1060,14 +1044,13 @@ impl Engine<'_> { // xxx.lhs[idx_expr].rhs // TODO - Allow chaining of indexing! - #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => match lhs.as_ref() { // xxx.id[idx_expr].rhs Expr::Property(id, pos) => { let fn_name = make_getter(id); self.call_fn_raw(None, fn_lib, &fn_name, &mut [this_ptr], None, *pos, 0) .and_then(|v| { - let (mut value, _, idx) = self.get_indexed_value( + let (mut value, _, index) = self.get_indexed_value( scope, fn_lib, &v, idx_expr, *op_pos, level, )?; @@ -1078,7 +1061,7 @@ impl Engine<'_> { )?; // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. - Self::update_indexed_value(v, idx, value, val_pos) + Self::update_indexed_value(v, index, value, val_pos) }) .and_then(|mut v| { let fn_name = make_setter(id); @@ -1110,7 +1093,6 @@ impl Engine<'_> { } // Evaluate a dot chain setter - #[cfg(not(feature = "no_object"))] fn set_dot_val( &self, scope: &mut Scope, @@ -1148,9 +1130,8 @@ impl Engine<'_> { // lhs[idx_expr].??? // TODO - Allow chaining of indexing! - #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx_expr, op_pos) => { - let (idx_src_type, src, idx, mut target) = + let (idx_src_type, src, index, mut target) = self.eval_index_expr(scope, fn_lib, lhs, idx_expr, *op_pos, level)?; let val_pos = new_val.1; let this_ptr = target.as_mut(); @@ -1171,7 +1152,7 @@ impl Engine<'_> { idx_src_type, scope, src, - idx, + index, (target, val_pos), )?; } @@ -1201,56 +1182,46 @@ impl Engine<'_> { let mut lhs_value = self.eval_expr(scope, fn_lib, lhs, level)?; let rhs_value = self.eval_expr(scope, fn_lib, rhs, level)?; - #[cfg(not(feature = "no_index"))] - { - if rhs_value.is::() { - let mut rhs_value = rhs_value.cast::(); - let def_value = false.into_dynamic(); - let mut result = false; + if rhs_value.is::() { + let mut rhs_value = rhs_value.cast::(); + let def_value = false.into_dynamic(); + let mut result = false; - // Call the '==' operator to compare each value - for value in rhs_value.iter_mut() { - let args = &mut [lhs_value.as_mut(), value.as_mut()]; - let def_value = Some(&def_value); - if self - .call_fn_raw(None, fn_lib, "==", args, def_value, rhs.position(), level)? - .try_cast::() - .unwrap_or(false) - { - result = true; - break; - } + // Call the '==' operator to compare each value + for value in rhs_value.iter_mut() { + let args = &mut [lhs_value.as_mut(), value.as_mut()]; + let def_value = Some(&def_value); + if self + .call_fn_raw(None, fn_lib, "==", args, def_value, rhs.position(), level)? + .try_cast::() + .unwrap_or(false) + { + result = true; + break; } - - return Ok(result.into_dynamic()); } - } - #[cfg(not(feature = "no_object"))] - { - if rhs_value.is::() { - let rhs_value = rhs_value.cast::(); + Ok(result.into_dynamic()) + } else if rhs_value.is::() { + let rhs_value = rhs_value.cast::(); - // Only allows String or char - return if lhs_value.is::() { - Ok(rhs_value - .contains_key(&lhs_value.cast::()) - .into_dynamic()) - } else if lhs_value.is::() { - Ok(rhs_value - .contains_key(&lhs_value.cast::().to_string()) - .into_dynamic()) - } else { - Err(EvalAltResult::ErrorInExpr(lhs.position())) - }; + // Only allows String or char + if lhs_value.is::() { + Ok(rhs_value + .contains_key(&lhs_value.cast::()) + .into_dynamic()) + } else if lhs_value.is::() { + Ok(rhs_value + .contains_key(&lhs_value.cast::().to_string()) + .into_dynamic()) + } else { + Err(EvalAltResult::ErrorInExpr(lhs.position())) } - } - - if rhs_value.is::() { + } else if rhs_value.is::() { let rhs_value = rhs_value.cast::(); // Only allows String or char - return if lhs_value.is::() { + if lhs_value.is::() { Ok(rhs_value .contains(&lhs_value.cast::()) .into_dynamic()) @@ -1258,10 +1229,10 @@ impl Engine<'_> { Ok(rhs_value.contains(lhs_value.cast::()).into_dynamic()) } else { Err(EvalAltResult::ErrorInExpr(lhs.position())) - }; + } + } else { + Err(EvalAltResult::ErrorInExpr(rhs.position())) } - - return Err(EvalAltResult::ErrorInExpr(rhs.position())); } /// Evaluate an expression @@ -1273,21 +1244,13 @@ impl Engine<'_> { level: usize, ) -> Result { match expr { - #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(f, _) => Ok(f.into_dynamic()), - Expr::IntegerConstant(i, _) => Ok(i.into_dynamic()), + Expr::FloatConstant(f, _) => Ok(f.into_dynamic()), Expr::StringConstant(s, _) => Ok(s.clone().into_owned().into_dynamic()), Expr::CharConstant(c, _) => Ok(c.into_dynamic()), Expr::Variable(id, pos) => Self::search_scope(scope, id, *pos).map(|(_, val)| val), Expr::Property(_, _) => panic!("unexpected property."), - // lhs[idx_expr] - #[cfg(not(feature = "no_index"))] - Expr::Index(lhs, idx_expr, op_pos) => self - .eval_index_expr(scope, fn_lib, lhs, idx_expr, *op_pos, level) - .map(|(_, _, _, x)| x), - // Statement block Expr::Stmt(stmt, _) => self.eval_stmt(scope, fn_lib, stmt, level), @@ -1312,7 +1275,6 @@ impl Engine<'_> { } => { // Avoid referencing scope which is used below as mut let entry = ScopeSource { name, ..entry }; - *scope.get_mut(entry) = rhs_val.clone(); Ok(rhs_val) } @@ -1329,7 +1291,7 @@ impl Engine<'_> { // idx_lhs[idx_expr] = rhs #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, idx_expr, op_pos) => { - let (idx_src_type, src, idx, _) = + let (idx_src_type, src, index, _) = self.eval_index_expr(scope, fn_lib, idx_lhs, idx_expr, *op_pos, level)?; if let Some(src) = src { @@ -1344,7 +1306,7 @@ impl Engine<'_> { idx_src_type, scope, src, - idx, + index, (rhs_val, rhs.position()), )?), } @@ -1373,6 +1335,12 @@ impl Engine<'_> { } } + // lhs[idx_expr] + #[cfg(not(feature = "no_index"))] + Expr::Index(lhs, idx_expr, op_pos) => self + .eval_index_expr(scope, fn_lib, lhs, idx_expr, *op_pos, level) + .map(|(_, _, _, x)| x), + #[cfg(not(feature = "no_object"))] Expr::Dot(lhs, rhs, _) => self.get_dot_val(scope, fn_lib, lhs, rhs, level), @@ -1417,35 +1385,14 @@ impl Engine<'_> { } match fn_name.as_ref() { - // Dump AST - KEYWORD_DUMP_AST => { - let pos = if args_expr_list.is_empty() { - *pos - } else { - args_expr_list[0].position() - }; - - // Change the argument to a debug dump of the expressions - let mut result = args_expr_list - .iter() - .map(|expr| format!("{:#?}", expr)) - .collect::>() - .join("\n") - .into_dynamic(); - - // Redirect call to `print` - let mut args = [result.as_mut()]; - self.call_fn_raw(None, fn_lib, KEYWORD_PRINT, &mut args, None, pos, level) - } - // type_of KEYWORD_TYPE_OF if args_expr_list.len() == 1 && !has_override(self, fn_lib, KEYWORD_TYPE_OF) => { - let r = self.eval_expr(scope, fn_lib, &args_expr_list[0], level)?; + let result = self.eval_expr(scope, fn_lib, &args_expr_list[0], level)?; Ok(self - .map_type_name((*r).type_name()) + .map_type_name((*result).type_name()) .to_string() .into_dynamic()) } @@ -1456,18 +1403,18 @@ impl Engine<'_> { && !has_override(self, fn_lib, KEYWORD_EVAL) => { let pos = args_expr_list[0].position(); - let r = self.eval_expr(scope, fn_lib, &args_expr_list[0], level)?; + let result = self.eval_expr(scope, fn_lib, &args_expr_list[0], level)?; // Get the script text by evaluating the expression - let script = - r.downcast_ref::() - .map(String::as_str) - .ok_or_else(|| { - EvalAltResult::ErrorMismatchOutputType( - r.type_name().into(), - pos, - ) - })?; + let script = result + .downcast_ref::() + .map(String::as_str) + .ok_or_else(|| { + EvalAltResult::ErrorMismatchOutputType( + result.type_name().into(), + pos, + ) + })?; // Compile the script text // No optimizations because we only run it once @@ -1475,19 +1422,15 @@ impl Engine<'_> { .compile_with_scope_and_optimization_level( &Scope::new(), script, - #[cfg(not(feature = "no_optimize"))] OptimizationLevel::None, ) .map_err(EvalAltResult::ErrorParsing)?; // If new functions are defined within the eval string, it is an error - #[cfg(not(feature = "no_function"))] - { - if ast.1.len() > 0 { - return Err(EvalAltResult::ErrorParsing( - ParseErrorType::WrongFnDefinition.into_err(pos), - )); - } + if ast.1.len() > 0 { + return Err(EvalAltResult::ErrorParsing( + ParseErrorType::WrongFnDefinition.into_err(pos), + )); } if let Some(lib) = fn_lib { @@ -1559,6 +1502,8 @@ impl Engine<'_> { Expr::True(_) => Ok(true.into_dynamic()), Expr::False(_) => Ok(false.into_dynamic()), Expr::Unit(_) => Ok(().into_dynamic()), + + expr => panic!("should not appear: {:?}", expr), } } @@ -1753,7 +1698,7 @@ impl Engine<'_> { } /// Print/debug to stdout -#[cfg(not(feature = "no_std"))] fn default_print(s: &str) { + #[cfg(not(feature = "no_std"))] println!("{}", s); } diff --git a/src/error.rs b/src/error.rs index 5ff11832..6ad21bbd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -37,6 +37,10 @@ impl fmt::Display for LexError { } /// Type of error encountered when parsing a script. +/// +/// Some errors never appear when certain features are turned on. +/// They still exist so that the application can turn features on and off without going through +/// massive code changes to remove/add back enum variants in match statements. #[derive(Debug, PartialEq, Clone)] pub enum ParseErrorType { /// Error in the script text. Wrapped value is the error message. @@ -51,19 +55,21 @@ pub enum ParseErrorType { MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. Wrapped value is the error description (if any). /// - /// Not available under the `no_index` feature. - #[cfg(not(feature = "no_index"))] + /// Never appears under the `no_index` feature. MalformedIndexExpr(String), /// An expression in an `in` expression has syntax error. Wrapped value is the error description (if any). + /// + /// Never appears under the `no_object` and `no_index` features combination. MalformedInExpr(String), /// A map definition has duplicated property names. Wrapped value is the property name. /// - /// Not available under the `no_object` feature. - #[cfg(not(feature = "no_object"))] + /// Never appears under the `no_object` feature. DuplicatedProperty(String), /// Invalid expression assigned to constant. Wrapped value is the name of the constant. ForbiddenConstantExpr(String), /// Missing a property name for custom types and maps. + /// + /// Never appears under the `no_object` feature. PropertyExpected, /// Missing a variable name after the `let`, `const` or `for` keywords. VariableExpected, @@ -71,28 +77,23 @@ pub enum ParseErrorType { ExprExpected(String), /// Defining a function `fn` in an appropriate place (e.g. inside another function). /// - /// Not available under the `no_function` feature. - #[cfg(not(feature = "no_function"))] + /// Never appears under the `no_function` feature. WrongFnDefinition, /// Missing a function name after the `fn` keyword. /// - /// Not available under the `no_function` feature. - #[cfg(not(feature = "no_function"))] + /// Never appears under the `no_function` feature. FnMissingName, /// A function definition is missing the parameters list. Wrapped value is the function name. /// - /// Not available under the `no_function` feature. - #[cfg(not(feature = "no_function"))] + /// Never appears under the `no_function` feature. FnMissingParams(String), /// A function definition has duplicated parameters. Wrapped values are the function name and parameter name. /// - /// Not available under the `no_function` feature. - #[cfg(not(feature = "no_function"))] + /// Never appears under the `no_function` feature. FnDuplicatedParam(String, String), /// A function definition is missing the body. Wrapped value is the function name. /// - /// Not available under the `no_function` feature. - #[cfg(not(feature = "no_function"))] + /// Never appears under the `no_function` feature. FnMissingBody(String), /// Assignment to an inappropriate LHS (left-hand-side) expression. AssignmentToInvalidLHS, @@ -138,24 +139,17 @@ impl ParseError { ParseErrorType::UnknownOperator(_) => "Unknown operator", ParseErrorType::MissingToken(_, _) => "Expecting a certain token that is missing", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", - #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", ParseErrorType::MalformedInExpr(_) => "Invalid 'in' expression", - #[cfg(not(feature = "no_object"))] ParseErrorType::DuplicatedProperty(_) => "Duplicated property in object map literal", ParseErrorType::ForbiddenConstantExpr(_) => "Expecting a constant", ParseErrorType::PropertyExpected => "Expecting name of a property", ParseErrorType::VariableExpected => "Expecting name of a variable", ParseErrorType::ExprExpected(_) => "Expecting an expression", - #[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::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration", - #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration", - #[cfg(not(feature = "no_function"))] ParseErrorType::WrongFnDefinition => "Function definitions must be at global 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", @@ -178,7 +172,6 @@ impl fmt::Display for ParseError { } ParseErrorType::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s)?, - #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(s) => { write!(f, "{}", if s.is_empty() { self.desc() } else { s })? } @@ -187,24 +180,20 @@ impl fmt::Display for ParseError { write!(f, "{}", if s.is_empty() { self.desc() } else { s })? } - #[cfg(not(feature = "no_object"))] ParseErrorType::DuplicatedProperty(s) => { write!(f, "Duplicated property '{}' for object map literal", s)? } ParseErrorType::ExprExpected(s) => write!(f, "Expecting {} expression", s)?, - #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(s) => { write!(f, "Expecting parameters for function '{}'", s)? } - #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingBody(s) => { write!(f, "Expecting body statement block for function '{}'", s)? } - #[cfg(not(feature = "no_function"))] ParseErrorType::FnDuplicatedParam(s, arg) => { write!(f, "Duplicated parameter '{}' for function '{}'", arg, s)? } diff --git a/src/optimize.rs b/src/optimize.rs index 86950423..fef42473 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,9 +1,7 @@ -#![cfg(not(feature = "no_optimize"))] - use crate::any::{Any, Dynamic}; use crate::engine::{ - Engine, FnAny, FnCallArgs, FnSpec, FunctionsLib, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_EVAL, - KEYWORD_PRINT, KEYWORD_TYPE_OF, + Engine, FnAny, FnCallArgs, FnSpec, FunctionsLib, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, + KEYWORD_TYPE_OF, }; use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Position, ReturnType, Stmt, AST}; use crate::result::EvalAltResult; @@ -33,6 +31,17 @@ pub enum OptimizationLevel { Full, } +impl OptimizationLevel { + /// Is the `OptimizationLevel` None. + pub fn is_none(self) -> bool { + self == Self::None + } + /// Is the `OptimizationLevel` Full. + pub fn is_full(self) -> bool { + self == Self::Full + } +} + /// Mutable state throughout an optimization pass. struct State<'a> { /// Has the AST been changed during this pass? @@ -519,10 +528,6 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> 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), - // Do not call some special keywords Expr::FunctionCall(id, args, def_value, pos) if DONT_EVAL_KEYWORDS.contains(&id.as_ref())=> Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), @@ -587,7 +592,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { } } -pub(crate) fn optimize<'a>( +fn optimize<'a>( statements: Vec, engine: &Engine<'a>, scope: &Scope, @@ -675,6 +680,9 @@ pub fn optimize_into_ast( functions: Vec, level: OptimizationLevel, ) -> AST { + #[cfg(feature = "no_optimize")] + const level: OptimizationLevel = OptimizationLevel::None; + let fn_lib: Vec<_> = functions .iter() .map(|fn_def| (fn_def.name.as_str(), fn_def.params.len())) @@ -685,7 +693,7 @@ pub fn optimize_into_ast( .iter() .cloned() .map(|mut fn_def| { - if level != OptimizationLevel::None { + if !level.is_none() { let pos = fn_def.body.position(); // Optimize the function body diff --git a/src/parser.rs b/src/parser.rs index 569d0ac4..07d5433c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,12 +1,10 @@ //! Main module defining the lexer and parser. use crate::any::{Any, AnyExt, Dynamic}; -use crate::engine::{Engine, FunctionsLib}; +use crate::engine::{Array, Engine, FunctionsLib, Map}; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::scope::{EntryType as ScopeEntryType, Scope}; - -#[cfg(not(feature = "no_optimize"))] use crate::optimize::{optimize_into_ast, OptimizationLevel}; +use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::stdlib::{ borrow::Cow, @@ -42,7 +40,6 @@ pub type INT = i32; /// The system floating-point type. /// /// Not available under the `no_float` feature. -#[cfg(not(feature = "no_float"))] pub type FLOAT = f64; type LERR = LexError; @@ -398,7 +395,6 @@ pub enum Expr { /// Integer constant. IntegerConstant(INT, Position), /// Floating-point constant. - #[cfg(not(feature = "no_float"))] FloatConstant(FLOAT, Position), /// Character constant. CharConstant(char, Position), @@ -415,16 +411,12 @@ pub enum Expr { /// expr = expr Assignment(Box, Box, Position), /// lhs.rhs - #[cfg(not(feature = "no_object"))] Dot(Box, Box, Position), /// expr[expr] - #[cfg(not(feature = "no_index"))] Index(Box, Box, Position), /// [ expr, ... ] - #[cfg(not(feature = "no_index"))] Array(Vec, Position), /// #{ name:expr, ... } - #[cfg(not(feature = "no_object"))] Map(Vec<(String, Expr, Position)>, Position), /// lhs in rhs In(Box, Box, Position), @@ -449,29 +441,25 @@ impl Expr { pub fn get_constant_value(&self) -> Dynamic { match self { Self::IntegerConstant(i, _) => i.into_dynamic(), + Self::FloatConstant(f, _) => f.into_dynamic(), Self::CharConstant(c, _) => c.into_dynamic(), Self::StringConstant(s, _) => s.clone().into_owned().into_dynamic(), Self::True(_) => true.into_dynamic(), Self::False(_) => false.into_dynamic(), Self::Unit(_) => ().into_dynamic(), - #[cfg(not(feature = "no_index"))] Self::Array(items, _) if items.iter().all(Self::is_constant) => items .iter() .map(Self::get_constant_value) .collect::>() .into_dynamic(), - #[cfg(not(feature = "no_object"))] Self::Map(items, _) if items.iter().all(|(_, v, _)| v.is_constant()) => items .iter() .map(|(k, v, _)| (k.clone(), v.get_constant_value())) .collect::>() .into_dynamic(), - #[cfg(not(feature = "no_float"))] - Self::FloatConstant(f, _) => f.into_dynamic(), - _ => panic!("cannot get value of non-constant expression"), } } @@ -484,18 +472,15 @@ impl Expr { pub fn get_constant_str(&self) -> String { match self { Self::IntegerConstant(i, _) => i.to_string(), + Self::FloatConstant(f, _) => f.to_string(), Self::CharConstant(c, _) => c.to_string(), Self::StringConstant(_, _) => "string".to_string(), Self::True(_) => "true".to_string(), Self::False(_) => "false".to_string(), Self::Unit(_) => "()".to_string(), - #[cfg(not(feature = "no_index"))] Self::Array(items, _) if items.iter().all(Self::is_constant) => "array".to_string(), - #[cfg(not(feature = "no_float"))] - Self::FloatConstant(f, _) => f.to_string(), - _ => panic!("cannot get value of non-constant expression"), } } @@ -504,8 +489,11 @@ impl Expr { pub fn position(&self) -> Position { match self { Self::IntegerConstant(_, pos) + | Self::FloatConstant(_, pos) | Self::CharConstant(_, pos) | Self::StringConstant(_, pos) + | Self::Array(_, pos) + | Self::Map(_, pos) | Self::Variable(_, pos) | Self::Property(_, pos) | Self::Stmt(_, pos) @@ -517,22 +505,9 @@ impl Expr { | Self::False(pos) | Self::Unit(pos) => *pos, - Self::Assignment(expr, _, _) => expr.position(), - - #[cfg(not(feature = "no_object"))] - Self::Dot(expr, _, _) => expr.position(), - - #[cfg(not(feature = "no_float"))] - Self::FloatConstant(_, pos) => *pos, - - #[cfg(not(feature = "no_index"))] - Self::Array(_, pos) => *pos, - - #[cfg(not(feature = "no_object"))] - Self::Map(_, pos) => *pos, - - #[cfg(not(feature = "no_index"))] - Self::Index(expr, _, _) => expr.position(), + Self::Assignment(expr, _, _) | Self::Dot(expr, _, _) | Self::Index(expr, _, _) => { + expr.position() + } } } @@ -541,13 +516,9 @@ impl Expr { /// A pure expression has no side effects. pub fn is_pure(&self) -> bool { match self { - #[cfg(not(feature = "no_index"))] Self::Array(expressions, _) => expressions.iter().all(Self::is_pure), - #[cfg(not(feature = "no_index"))] - Self::Index(x, y, _) => x.is_pure() && y.is_pure(), - - Self::And(x, y, _) | Self::Or(x, y, _) | Self::In(x, y, _) => { + Self::Index(x, y, _) | Self::And(x, y, _) | Self::Or(x, y, _) | Self::In(x, y, _) => { x.is_pure() && y.is_pure() } @@ -561,21 +532,17 @@ impl Expr { pub fn is_constant(&self) -> bool { match self { Self::IntegerConstant(_, _) + | Self::FloatConstant(_, _) | Self::CharConstant(_, _) | Self::StringConstant(_, _) | Self::True(_) | Self::False(_) | Self::Unit(_) => true, - #[cfg(not(feature = "no_float"))] - Self::FloatConstant(_, _) => true, - // An array literal is constant if all items are constant - #[cfg(not(feature = "no_index"))] Self::Array(expressions, _) => expressions.iter().all(Self::is_constant), // An map literal is constant if all items are constant - #[cfg(not(feature = "no_object"))] Self::Map(items, _) => items.iter().map(|(_, expr, _)| expr).all(Self::is_constant), // Check in expression @@ -594,7 +561,6 @@ impl Expr { #[derive(Debug, PartialEq, Clone)] pub enum Token { IntegerConstant(INT), - #[cfg(not(feature = "no_float"))] FloatConstant(FLOAT), Identifier(String), CharConstant(char), @@ -603,9 +569,7 @@ pub enum Token { RightBrace, LeftParen, RightParen, - #[cfg(not(feature = "no_index"))] LeftBracket, - #[cfg(not(feature = "no_index"))] RightBracket, Plus, UnaryPlus, @@ -673,7 +637,6 @@ impl Token { match self { IntegerConstant(i) => i.to_string().into(), - #[cfg(not(feature = "no_float"))] FloatConstant(f) => f.to_string().into(), Identifier(s) => s.into(), CharConstant(c) => c.to_string().into(), @@ -685,9 +648,7 @@ impl Token { RightBrace => "}", LeftParen => "(", RightParen => ")", - #[cfg(not(feature = "no_index"))] LeftBracket => "[", - #[cfg(not(feature = "no_index"))] RightBracket => "]", Plus => "+", UnaryPlus => "+", @@ -762,6 +723,8 @@ 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 | @@ -805,10 +768,6 @@ impl Token { In | PowerOfAssign => true, - #[cfg(not(feature = "no_index"))] - LeftBracket => true, // [-expr] - is unary - // RightBracket | [expr] - expr not unary & is closing - _ => false, } } @@ -1211,9 +1170,7 @@ impl<'a> TokenIterator<'a> { (')', _) => return Some((Token::RightParen, pos)), // Indexing - #[cfg(not(feature = "no_index"))] ('[', _) => return Some((Token::LeftBracket, pos)), - #[cfg(not(feature = "no_index"))] (']', _) => return Some((Token::RightBracket, pos)), // Map literal @@ -1506,7 +1463,6 @@ fn parse_call_expr<'a, S: Into> + Display>( } /// Parse an indexing expression. -#[cfg(not(feature = "no_index"))] fn parse_index_expr<'a>( lhs: Box, input: &mut Peekable>, @@ -1528,7 +1484,6 @@ fn parse_index_expr<'a>( Expr::IntegerConstant(_, pos) => match *lhs { Expr::Array(_, _) | Expr::StringConstant(_, _) => (), - #[cfg(not(feature = "no_object"))] Expr::Map(_, _) => { return Err(PERR::MalformedIndexExpr( "Object map access expects string index, not a number".into(), @@ -1556,7 +1511,6 @@ fn parse_index_expr<'a>( // lhs[string] Expr::StringConstant(_, pos) => match *lhs { - #[cfg(not(feature = "no_object"))] Expr::Map(_, _) => (), Expr::Array(_, _) | Expr::StringConstant(_, _) => { @@ -1584,7 +1538,6 @@ fn parse_index_expr<'a>( }, // lhs[float] - #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a float".into(), @@ -1673,7 +1626,6 @@ fn parse_ident_expr<'a, S: Into> + Display>( } /// Parse an array literal. -#[cfg(not(feature = "no_index"))] fn parse_array_literal<'a>( input: &mut Peekable>, begin: Position, @@ -1715,7 +1667,6 @@ fn parse_array_literal<'a>( } /// Parse a map literal. -#[cfg(not(feature = "no_object"))] fn parse_map_literal<'a>( input: &mut Peekable>, begin: Position, @@ -1840,10 +1791,8 @@ fn parse_primary<'a>( let mut can_be_indexed = false; let mut root_expr = match token { - #[cfg(not(feature = "no_float"))] - (Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)), - (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), + (Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)), (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), (Token::StringConst(s), pos) => { can_be_indexed = true; @@ -1875,9 +1824,11 @@ fn parse_primary<'a>( } }?; + #[cfg(feature = "no_index")] + let can_be_indexed = false; + if can_be_indexed { // Tail processing all possible indexing - #[cfg(not(feature = "no_index"))] while let Some((Token::LeftBracket, pos)) = input.peek() { let pos = *pos; input.next(); @@ -1918,10 +1869,13 @@ fn parse_unary<'a>( .map(|x| Expr::IntegerConstant(x, pos)) .or_else(|| { #[cfg(not(feature = "no_float"))] - return Some(Expr::FloatConstant(-(i as FLOAT), pos)); - + { + Some(Expr::FloatConstant(-(i as FLOAT), pos)) + } #[cfg(feature = "no_float")] - return None; + { + None + } }) .ok_or_else(|| { PERR::BadInput(LERR::MalformedNumber(format!("-{}", i)).to_string()) @@ -1976,48 +1930,41 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result { assert!(is_top, "property expected but gets variable"); None } // property[...] - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) => { assert!(!is_top, "variable expected but gets property"); None } // idx_lhs[...] - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, pos) => match idx_lhs.as_ref() { Expr::Index(_, _, _) => Some(ParseErrorType::AssignmentToCopy.into_err(*pos)), _ => Some(ParseErrorType::AssignmentToInvalidLHS.into_err(*pos)), }, // dot_lhs.dot_rhs - #[cfg(not(feature = "no_object"))] Expr::Dot(dot_lhs, dot_rhs, _) => match dot_lhs.as_ref() { // var.dot_rhs Expr::Variable(_, _) if is_top => valid_assignment_chain(dot_rhs, false), // property.dot_rhs Expr::Property(_, _) if !is_top => valid_assignment_chain(dot_rhs, false), // var[...] - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) && is_top => { valid_assignment_chain(dot_rhs, false) } // property[...] - #[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) } // idx_lhs[...] - #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) => { Some(ParseErrorType::AssignmentToCopy.into_err(idx_lhs.position())) } @@ -2055,15 +2002,8 @@ fn parse_op_assignment>>( /// Parse an 'in' expression. fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { match (&lhs, &rhs) { - #[cfg(not(feature = "no_float"))] - (_, Expr::FloatConstant(_, pos)) => { - return Err(PERR::MalformedInExpr( - "'in' expression expects a string, array or object map".into(), - ) - .into_err(*pos)) - } - (_, Expr::IntegerConstant(_, pos)) + | (_, Expr::FloatConstant(_, pos)) | (_, Expr::And(_, _, pos)) | (_, Expr::Or(_, _, pos)) | (_, Expr::In(_, _, pos)) @@ -2082,7 +2022,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result (), // 123.456 in "xxxx" - #[cfg(not(feature = "no_float"))] (Expr::FloatConstant(_, pos), Expr::StringConstant(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not a float".into(), @@ -2109,7 +2048,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not an array".into(), @@ -2117,7 +2055,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { return Err(PERR::MalformedInExpr( "'in' expression for a string expects a string, not an object map".into(), @@ -2134,13 +2071,10 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result (), // 123.456 in #{...} - #[cfg(not(feature = "no_float"))] - #[cfg(not(feature = "no_object"))] (Expr::FloatConstant(_, pos), Expr::Map(_, _)) => { return Err(PERR::MalformedInExpr( "'in' expression for an object map expects a string, not a float".into(), @@ -2148,7 +2082,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { return Err(PERR::MalformedInExpr( "'in' expression for an object map expects a string, not a number".into(), @@ -2157,7 +2090,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result Result { return Err(PERR::MalformedInExpr( "'in' expression for an object map expects a string, not an array".into(), @@ -2178,7 +2108,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { return Err(PERR::MalformedInExpr( "'in' expression for an object map expects a string, not an object map".into(), @@ -2186,7 +2115,6 @@ fn parse_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { return Err(PERR::MalformedInExpr( "'in' expression for an object map expects a string, not ()".into(), @@ -2269,7 +2197,6 @@ fn parse_binary_op<'a>( pos, )), // xxx.lhs[idx] - #[cfg(not(feature = "no_index"))] Expr::Index(lhs, idx, pos) => { Ok(Expr::Index(Box::new(check_property(*lhs)?), idx, pos)) } @@ -2290,37 +2217,37 @@ fn parse_binary_op<'a>( Token::EqualsTo => Expr::FunctionCall( "==".into(), vec![current_lhs, rhs], - Some(Box::new(false)), + Some((false).into_dynamic()), pos, ), Token::NotEqualsTo => Expr::FunctionCall( "!=".into(), vec![current_lhs, rhs], - Some(Box::new(false)), + Some((false).into_dynamic()), pos, ), Token::LessThan => Expr::FunctionCall( "<".into(), vec![current_lhs, rhs], - Some(Box::new(false)), + Some((false).into_dynamic()), pos, ), Token::LessThanEqualsTo => Expr::FunctionCall( "<=".into(), vec![current_lhs, rhs], - Some(Box::new(false)), + Some((false).into_dynamic()), pos, ), Token::GreaterThan => Expr::FunctionCall( ">".into(), vec![current_lhs, rhs], - Some(Box::new(false)), + Some((false).into_dynamic()), pos, ), Token::GreaterThanEqualsTo => Expr::FunctionCall( ">=".into(), vec![current_lhs, rhs], - Some(Box::new(false)), + Some((false).into_dynamic()), pos, ), @@ -2680,7 +2607,6 @@ fn parse_stmt<'a>( } /// Parse a function definition. -#[cfg(not(feature = "no_function"))] fn parse_fn<'a>( input: &mut Peekable>, allow_stmt_expr: bool, @@ -2780,7 +2706,6 @@ pub fn parse_global_expr<'a, 'e>( Ok( // Optimize AST - #[cfg(not(feature = "no_optimize"))] optimize_into_ast( engine, scope, @@ -2788,20 +2713,6 @@ pub fn parse_global_expr<'a, 'e>( vec![], engine.optimization_level, ), - // - // Do not optimize AST if `no_optimize` - #[cfg(feature = "no_optimize")] - AST( - vec![Stmt::Expr(Box::new(expr))], - #[cfg(feature = "sync")] - { - Arc::new(FunctionsLib::new()) - }, - #[cfg(not(feature = "sync"))] - { - Rc::new(FunctionsLib::new()) - }, - ), ) } @@ -2813,9 +2724,9 @@ fn parse_global_level<'a>( let mut functions = Vec::::new(); while input.peek().is_some() { + // Collect all the function definitions #[cfg(not(feature = "no_function"))] { - // Collect all the function definitions if matches!(input.peek().expect("should not be None"), (Token::Fn, _)) { let f = parse_fn(input, true)?; @@ -2866,18 +2777,13 @@ pub fn parse<'a, 'e>( input: &mut Peekable>, engine: &Engine<'e>, scope: &Scope, - #[cfg(not(feature = "no_optimize"))] optimization_level: OptimizationLevel, + optimization_level: OptimizationLevel, ) -> Result { let (statements, functions) = parse_global_level(input)?; Ok( // Optimize AST - #[cfg(not(feature = "no_optimize"))] optimize_into_ast(engine, scope, statements, functions, optimization_level), - // - // Do not optimize AST if `no_optimize` - #[cfg(feature = "no_optimize")] - AST(statements, Arc::new(FunctionsLib::from_vec(functions))), ) } @@ -2898,6 +2804,47 @@ pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> Option { Expr::False(pos) }) } else { + #[cfg(not(feature = "no_index"))] + { + if value.is::() { + let array = value.cast::(); + let items: Vec<_> = array + .into_iter() + .map(|x| map_dynamic_to_expr(x, pos)) + .collect(); + if items.iter().all(Option::is_some) { + return Some(Expr::Array( + items.into_iter().map(Option::unwrap).collect(), + pos, + )); + } else { + return None; + } + } + } + + #[cfg(not(feature = "no_object"))] + { + if value.is::() { + let map = value.cast::(); + let items: Vec<_> = map + .into_iter() + .map(|(k, v)| (k, map_dynamic_to_expr(v, pos), pos)) + .collect(); + if items.iter().all(|(_, expr, _)| expr.is_some()) { + return Some(Expr::Map( + items + .into_iter() + .map(|(k, expr, pos)| (k, expr.unwrap(), pos)) + .collect(), + pos, + )); + } else { + return None; + } + } + } + #[cfg(not(feature = "no_float"))] { if value.is::() { diff --git a/src/result.rs b/src/result.rs index 29af3bbd..ce2b1b87 100644 --- a/src/result.rs +++ b/src/result.rs @@ -25,7 +25,7 @@ pub enum EvalAltResult { /// Error reading from a script file. Wrapped value is the path of the script file. /// - /// Not available under the `no_std` feature. + /// Never appears under the `no_std` feature. #[cfg(not(feature = "no_std"))] ErrorReadingScriptFile(PathBuf, std::io::Error), From 5d611d167484dc320d1445953352acc937f0f68c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 15:18:26 +0800 Subject: [PATCH 13/21] Add docs on JSON parsing. --- README.md | 101 +++++++++++++++++++++++++------ src/builtin.rs | 2 + tests/maps.rs | 157 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 220 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 9393fd1b..b200f5ec 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Optional features | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `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 not needed. | -| `no_index` | Disable arrays and indexing features if not needed. | +| `no_index` | Disable [arrays] and indexing features if not needed. | | `no_object` | Disable support for custom types and objects. | | `no_float` | Disable floating-point numbers and math if not needed. | | `no_optimize` | Disable the script optimizer. | @@ -102,7 +102,7 @@ A number of examples can be found in the `examples` folder: | Example | Description | | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of arrays on it | +| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of [arrays] on it | | [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a type and methods for it | | [`hello`](examples/hello.rs) | simple example that evaluates an expression and prints the result | | [`no_std`](examples/no_std.rs) | example to test out `no-std` builds | @@ -127,7 +127,7 @@ There are also a number of examples scripts that showcase Rhai's features, all i | Language feature scripts | Description | | ---------------------------------------------------- | ------------------------------------------------------------- | -| [`array.rhai`](scripts/array.rhai) | arrays in Rhai | +| [`array.rhai`](scripts/array.rhai) | [arrays] in Rhai | | [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | | [`comments.rhai`](scripts/comments.rhai) | just comments | | [`for1.rhai`](scripts/for1.rhai) | for loops | @@ -139,7 +139,7 @@ There are also a number of examples scripts that showcase Rhai's features, all i | [`op1.rhai`](scripts/op1.rhai) | just a simple addition | | [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | | [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | -| [`string.rhai`](scripts/string.rhai) | string operations | +| [`string.rhai`](scripts/string.rhai) | [string] operations | | [`while.rhai`](scripts/while.rhai) | while loop | | Example scripts | Description | @@ -324,6 +324,9 @@ engine. Evaluate expressions only ------------------------- +[`eval_expression`]: #evaluate-expressions-only +[`eval_expression_with_scope`]: #evaluate-expressions-only + Sometimes a use case does not require a full-blown scripting _language_, but only needs to evaluate _expressions_. In these cases, use the `compile_expression` and `eval_expression` methods or their `_with_scope` variants. @@ -375,7 +378,7 @@ This is useful on some 32-bit systems where using 64-bit integers incurs a perfo If no floating-point is needed or supported, use the [`no_float`] feature to remove it. -The `to_string` function converts a standard type into a string for display purposes. +The `to_string` function converts a standard type into a [string] for display purposes. The `type_of` function detects the actual type of a value. This is useful because all variables are [`Dynamic`] in nature. @@ -429,8 +432,8 @@ if type_of(mystery) == "i64" { } ``` -In Rust, sometimes a `Dynamic` forms part of a returned value - a good example is an array with `Dynamic` elements, -or an object map with `Dynamic` property values. To get the _real_ values, the actual value types _must_ be known in advance. +In Rust, sometimes a `Dynamic` forms part of a returned value - a good example is an [array] with `Dynamic` elements, +or an [object map] with `Dynamic` property values. To get the _real_ values, the actual value types _must_ be known in advance. There is no easy way for Rust to decide, at run-time, what type the `Dynamic` value is (short of using the `type_name` function and match against the name). @@ -1063,6 +1066,10 @@ The following standard functions (defined in the standard library but excluded i Strings and Chars ----------------- +[string]: #strings-and-chars +[strings]: #strings-and-chars +[char]: #strings-and-chars + String and char literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and hex ('`\x`_xx_') escape sequences. @@ -1125,6 +1132,8 @@ record == "Bob X. Davis: age 42 ❤\n"; 'C' in record == false; ``` +### Built-in functions + The following standard functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on strings: | Function | Description | @@ -1138,7 +1147,7 @@ The following standard functions (defined in the standard library but excluded i | `replace` | replaces a substring with another | | `trim` | trims the string | -Examples: +### Examples ```rust let full_name == " Bob C. Davis "; @@ -1170,6 +1179,10 @@ full_name.len() == 0; Arrays ------ +[array]: #arrays +[arrays]: #arrays +[`Array`]: #arrays + 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 '`,`'. All elements stored in an array are [`Dynamic`], and the array can freely grow or shrink with elements added or removed. @@ -1178,6 +1191,8 @@ The Rust type of a Rhai array is `rhai::Array`. [`type_of()`] an array returns ` Arrays are disabled via the [`no_index`] feature. +### Built-in functions + The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on arrays: | Function | Description | @@ -1192,7 +1207,7 @@ The following functions (defined in the standard library but excluded if using a | `clear` | empties the array | | `truncate` | cuts off the array at exactly a specified length (discarding all subsequent elements) | -Examples: +### Examples ```rust let y = [1, 2, 3]; // array literal with 3 elements @@ -1259,14 +1274,17 @@ engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(i Object maps ----------- +[object map]: #object-maps +[object maps]: #object-maps + Object maps are dictionaries. Properties are all [`Dynamic`] and can be freely added and retrieved. Object map literals are built within braces '`#{`' ... '`}`' (_name_ `:` _value_ syntax similar to Rust) and separated by commas '`,`'. The property _name_ can be a simple variable name following the same -naming rules as [variables], or an arbitrary string literal. +naming rules as [variables], or an arbitrary [string] literal. Property values can be accessed via the dot notation (_object_ `.` _property_) or index notation (_object_ `[` _property_ `]`). The dot notation allows only property names that follow the same naming rules as [variables]. -The index notation allows setting/getting properties of arbitrary names (even the empty string). +The index notation allows setting/getting properties of arbitrary names (even the empty [string]). **Important:** Trying to read a non-existent property returns [`()`] instead of causing an error. @@ -1274,6 +1292,8 @@ The Rust type of a Rhai object map is `rhai::Map`. [`type_of()`] an object map r Object maps are disabled via the [`no_object`] feature. +### Built-in functions + The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on object maps: | Function | Description | @@ -1283,10 +1303,10 @@ The following functions (defined in the standard library but excluded if using a | `clear` | empties the object map | | `mixin` | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | | `+` operator | merges the first object map with the second | -| `keys` | returns an array of all the property names (in random order) | -| `values` | returns an array of all the property values (in random order) | +| `keys` | returns an [array] of all the property names (in random order) | +| `values` | returns an [array] of all the property values (in random order) | -Examples: +### Examples ```rust let y = #{ // object map literal with 3 properties @@ -1347,13 +1367,60 @@ y.clear(); // empty the object map print(y.len()); // prints 0 ``` +### Parsing from JSON + +The syntax for an object map is extremely similar to JSON, with the exception of `null` values which can +technically be mapped to [`()`]. A valid JSON string does not start with a hash character `#` while a +Rhai object map does - that's the major difference! + +JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if +the [`no_float`] feature is not turned on. Most common generators of JSON data distinguish between +integer and floating-point values by always serializing a floating-point number with a decimal point +(i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully +with Rhai object maps. + +Use the [`eval_expression`]`::` method (or [`eval_expression_with_scope`]`::` in order to +handle `null` values) to parse a piece of JSON (with the hash character `#` attached) into an object map: + +```rust +// JSON string - notice that JSON property names are always quoted +// notice also that comments are acceptable within the JSON string +let json = r#"{ + "a": 1, // <- this is an integer number + "b": true, + "c": 123.0, // <- this is a floating-point number + "$d e f!": "hello", // <- any text can be a property name + "^^^!!!": [1,42,"999"], // <- value can be array or another hash + "z": null // <- JSON 'null' value + } +"#; + +// Create a new scope +let mut scope = Scope::new(); +scope.push_constant("null", ()); // map 'null' to '()' + +// Parse the JSON expression as an object map by attaching '#' in front +let expr = format!("#{}", json); +let map = engine.eval_expression_with_scope::(&mut scope, expr)?; + +map.len() == 6; // 'map' contains all properties int the JSON string + +// Push the map back into the scope +scope.clear(); +scope.push("map", map); + +let result = engine.eval_with_scope::(r#"map["^^^!!!"].len()"#)?; + +result == 3; // the object map is used in a script +``` + Comparison operators -------------------- Comparing most values of the same data type work out-of-the-box for standard types supported by the system. However, if using a [raw `Engine`], 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`. +`INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), [string], [array], `bool`, `char`. ```rust 42 == 42; // true @@ -1411,7 +1478,7 @@ number <<= 2; // number = number << 2 number >>= 1; // number = number >> 1 ``` -The `+=` operator can also be used to build strings: +The `+=` operator can also be used to build [strings]: ```rust let my_str = "abc"; @@ -1488,7 +1555,7 @@ loop { `for` loops ----------- -Iterating through a range or an array is provided by the `for` ... `in` loop. +Iterating through a range or an [array] is provided by the `for` ... `in` loop. ```rust let array = [1, 3, 5, 7, 9, 42]; diff --git a/src/builtin.rs b/src/builtin.rs index f63f4fd5..dfc709c2 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -631,12 +631,14 @@ impl Engine<'_> { }); // Register map access functions + #[cfg(not(feature = "no_index"))] self.register_fn("keys", |map: Map| { map.into_iter() .map(|(k, _)| k.into_dynamic()) .collect::>() }); + #[cfg(not(feature = "no_index"))] self.register_fn("values", |map: Map| { map.into_iter().map(|(_, v)| v).collect::>() }); diff --git a/tests/maps.rs b/tests/maps.rs index b979f331..f6bc45db 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -1,6 +1,6 @@ #![cfg(not(feature = "no_object"))] -use rhai::{AnyExt, Engine, EvalAltResult, Map, INT}; +use rhai::{AnyExt, Engine, EvalAltResult, Map, Scope, INT}; #[test] fn test_map_indexing() -> Result<(), EvalAltResult> { @@ -75,13 +75,28 @@ fn test_map_assign() -> Result<(), EvalAltResult> { let engine = Engine::new(); let x = engine.eval::(r#"let x = #{a: 1, b: true, "c$": "hello"}; x"#)?; - let a = x.get("a").cloned().expect("should have property a"); - let b = x.get("b").cloned().expect("should have property b"); - let c = x.get("c$").cloned().expect("should have property c$"); - assert_eq!(a.cast::(), 1); - assert_eq!(b.cast::(), true); - assert_eq!(c.cast::(), "hello"); + assert_eq!( + x.get("a") + .cloned() + .expect("should have property a") + .cast::(), + 1 + ); + assert_eq!( + x.get("b") + .cloned() + .expect("should have property b") + .cast::(), + true + ); + assert_eq!( + x.get("c$") + .cloned() + .expect("should have property c$") + .cast::(), + "hello" + ); Ok(()) } @@ -91,13 +106,28 @@ fn test_map_return() -> Result<(), EvalAltResult> { let engine = Engine::new(); let x = engine.eval::(r#"#{a: 1, b: true, "c$": "hello"}"#)?; - let a = x.get("a").cloned().expect("should have property a"); - let b = x.get("b").cloned().expect("should have property b"); - let c = x.get("c$").cloned().expect("should have property c$"); - assert_eq!(a.cast::(), 1); - assert_eq!(b.cast::(), true); - assert_eq!(c.cast::(), "hello"); + assert_eq!( + x.get("a") + .cloned() + .expect("should have property a") + .cast::(), + 1 + ); + assert_eq!( + x.get("b") + .cloned() + .expect("should have property b") + .cast::(), + true + ); + assert_eq!( + x.get("c$") + .cloned() + .expect("should have property c$") + .cast::(), + "hello" + ); Ok(()) } @@ -107,20 +137,101 @@ fn test_map_for() -> Result<(), EvalAltResult> { let engine = Engine::new(); assert_eq!( - engine.eval::( - r#" - let map = #{a: 1, b: true, c: 123.456}; - let s = ""; + engine + .eval::( + r#" + let map = #{a: 1, b_x: true, "$c d e!": "hello"}; + let s = ""; - for key in keys(map) { - s += key; - } + for key in keys(map) { + s += key; + } - s.len() + s "# - )?, - 3 + )? + .len(), + 11 ); Ok(()) } + +#[test] +/// Because a Rhai object map literal is almost the same as JSON, +/// it is possible to convert from JSON into a Rhai object map. +fn test_map_json() -> Result<(), EvalAltResult> { + let engine = Engine::new(); + + let mut scope = Scope::new(); + scope.push_constant("null", ()); + scope.push_constant("undefined", ()); + + let json = r#"{"a":1, "b":true, "c":42, "$d e f!":"hello", "z":null}"#; + + let map = engine.eval_expression_with_scope::(&mut scope, &("#".to_string() + json))?; + + assert!(!map.contains_key("x")); + + assert_eq!( + map.get("a") + .cloned() + .expect("should have property a") + .cast::(), + 1 + ); + assert_eq!( + map.get("b") + .cloned() + .expect("should have property b") + .cast::(), + true + ); + assert_eq!( + map.get("c") + .cloned() + .expect("should have property a") + .cast::(), + 42 + ); + assert_eq!( + map.get("$d e f!") + .cloned() + .expect("should have property $d e f!") + .cast::(), + "hello" + ); + assert_eq!( + map.get("z") + .cloned() + .expect("should have property z") + .cast::<()>(), + () + ); + + #[cfg(not(feature = "no_index"))] + { + scope.clear(); + scope.push_constant("map", map); + + assert_eq!( + engine + .eval_with_scope::( + &mut scope, + r#" + let s = ""; + + for key in keys(map) { + s += key; + } + + s + "# + )? + .len(), + 11 + ); + } + + Ok(()) +} From ff8eca8a5e46254f8c1038c6869e6732f70172e2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 17:14:07 +0800 Subject: [PATCH 14/21] Add parse_json. --- README.md | 19 +++++-------- src/api.rs | 75 +++++++++++++++++++++++++++++++++++++-------------- src/engine.rs | 2 +- src/parser.rs | 65 ++++++++++++++++++++++++++++++-------------- tests/maps.rs | 8 ++---- 5 files changed, 110 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index b200f5ec..e4d6d354 100644 --- a/README.md +++ b/README.md @@ -1379,8 +1379,7 @@ integer and floating-point values by always serializing a floating-point number (i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully with Rhai object maps. -Use the [`eval_expression`]`::` method (or [`eval_expression_with_scope`]`::` in order to -handle `null` values) to parse a piece of JSON (with the hash character `#` attached) into an object map: +Use the `parse_json` method to parse a piece of JSON into an object map: ```rust // JSON string - notice that JSON property names are always quoted @@ -1395,23 +1394,19 @@ let json = r#"{ } "#; -// Create a new scope -let mut scope = Scope::new(); -scope.push_constant("null", ()); // map 'null' to '()' - -// Parse the JSON expression as an object map by attaching '#' in front -let expr = format!("#{}", json); -let map = engine.eval_expression_with_scope::(&mut scope, expr)?; +// Parse the JSON expression as an object map +// Set the second boolean parameter to true in order to map 'null' to '()' +let map = engine.parse_json(json, true)?; map.len() == 6; // 'map' contains all properties int the JSON string -// Push the map back into the scope -scope.clear(); +// Put the object map into a 'Scope' +let mut scope = Scope::new(); scope.push("map", map); let result = engine.eval_with_scope::(r#"map["^^^!!!"].len()"#)?; -result == 3; // the object map is used in a script +result == 3; // the object map is successfully used in the script ``` Comparison operators diff --git a/src/api.rs b/src/api.rs index a2c7755a..9b25a7cc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Any, AnyExt, Dynamic}; -use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec}; +use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec, Map}; use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_register::RegisterFn; @@ -395,14 +395,9 @@ impl<'e> Engine<'e> { script: &str, optimization_level: OptimizationLevel, ) -> Result { - let tokens_stream = lex(script); - - parse( - &mut tokens_stream.peekable(), - self, - scope, - optimization_level, - ) + let scripts = [script]; + let stream = lex(&scripts); + parse(&mut stream.peekable(), self, scope, optimization_level) } /// Read the contents of a file into a string. @@ -483,6 +478,51 @@ impl<'e> Engine<'e> { Self::read_file(path).and_then(|contents| Ok(self.compile_with_scope(scope, &contents)?)) } + /// Parse a JSON string into a map. + /// + /// Set `has_null` to `true` in order to map `null` values to `()`. + /// Setting it to `false` will cause a _variable not found_ error during parsing. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, AnyExt}; + /// + /// let engine = Engine::new(); + /// + /// let map = engine.parse_json(r#"{"a":123, "b":42, "c":false, "d":null}"#, true)?; + /// + /// assert_eq!(map.len(), 4); + /// assert_eq!(map.get("a").cloned().unwrap().cast::(), 123); + /// assert_eq!(map.get("b").cloned().unwrap().cast::(), 42); + /// assert_eq!(map.get("c").cloned().unwrap().cast::(), false); + /// assert_eq!(map.get("d").cloned().unwrap().cast::<()>(), ()); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + pub fn parse_json(&self, json: &str, has_null: bool) -> Result { + let mut scope = Scope::new(); + + // Trims the JSON string and add a '#' in front + let scripts = ["#", json.trim()]; + let stream = lex(&scripts); + let ast = parse_global_expr( + &mut stream.peekable(), + self, + &scope, + OptimizationLevel::None, + )?; + + // Handle null - map to () + if has_null { + scope.push_constant("null", ()); + } + + self.eval_ast_with_scope(&mut scope, &ast) + } + /// Compile a string containing an expression into an `AST`, /// which can be used later for evaluation. /// @@ -551,8 +591,9 @@ impl<'e> Engine<'e> { scope: &Scope, script: &str, ) -> Result { - let tokens_stream = lex(script); - parse_global_expr(&mut tokens_stream.peekable(), self, scope) + let scripts = [script]; + let stream = lex(&scripts); + parse_global_expr(&mut stream.peekable(), self, scope, self.optimization_level) } /// Evaluate a script file. @@ -807,15 +848,9 @@ impl<'e> Engine<'e> { /// 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. pub fn consume_with_scope(&self, scope: &mut Scope, script: &str) -> Result<(), EvalAltResult> { - let tokens_stream = lex(script); - - let ast = parse( - &mut tokens_stream.peekable(), - self, - scope, - self.optimization_level, - )?; - + let scripts = [script]; + let stream = lex(&scripts); + let ast = parse(&mut stream.peekable(), self, scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/engine.rs b/src/engine.rs index c308b236..5abe17eb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1503,7 +1503,7 @@ impl Engine<'_> { Expr::False(_) => Ok(false.into_dynamic()), Expr::Unit(_) => Ok(().into_dynamic()), - expr => panic!("should not appear: {:?}", expr), + _ => panic!("should not appear: {:?}", expr), } } diff --git a/src/parser.rs b/src/parser.rs index 07d5433c..21f04e1f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -828,16 +828,40 @@ pub struct TokenIterator<'a> { can_be_unary: bool, /// Current position. pos: Position, - /// The input characters stream. - stream: Peekable>, + /// The input character streams. + streams: Vec>>, } impl<'a> TokenIterator<'a> { /// Consume the next character. fn eat_next(&mut self) { - self.stream.next(); + self.get_next(); self.advance(); } + /// Get the next character + fn get_next(&mut self) -> Option { + loop { + if self.streams.is_empty() { + return None; + } else if let Some(ch) = self.streams[0].next() { + return Some(ch); + } else { + let _ = self.streams.remove(0); + } + } + } + /// Peek the next character + fn peek_next(&mut self) -> Option { + loop { + if self.streams.is_empty() { + return None; + } else if let Some(ch) = self.streams[0].peek() { + return Some(*ch); + } else { + let _ = self.streams.remove(0); + } + } + } /// Move the current position one character ahead. fn advance(&mut self) { self.pos.advance(); @@ -864,7 +888,7 @@ impl<'a> TokenIterator<'a> { let mut escape = String::with_capacity(12); loop { - let next_char = self.stream.next(); + let next_char = self.get_next(); self.advance(); match next_char.ok_or((LERR::UnterminatedString, Position::eof()))? { @@ -907,7 +931,7 @@ impl<'a> TokenIterator<'a> { }; for _ in 0..len { - let c = self.stream.next().ok_or_else(|| { + let c = self.get_next().ok_or_else(|| { (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) })?; @@ -958,12 +982,12 @@ impl<'a> TokenIterator<'a> { fn inner_next(&mut self) -> Option<(Token, Position)> { let mut negated = false; - while let Some(c) = self.stream.next() { + while let Some(c) = self.get_next() { self.advance(); let pos = self.pos; - match (c, self.stream.peek().copied().unwrap_or('\0')) { + match (c, self.peek_next().unwrap_or('\0')) { // \n ('\n', _) => self.new_line(), @@ -973,7 +997,7 @@ impl<'a> TokenIterator<'a> { let mut radix_base: Option = None; result.push(c); - while let Some(&next_char) = self.stream.peek() { + while let Some(next_char) = self.peek_next() { match next_char { '0'..='9' | '_' => { result.push(next_char); @@ -983,7 +1007,7 @@ impl<'a> TokenIterator<'a> { '.' => { result.push(next_char); self.eat_next(); - while let Some(&next_char_in_float) = self.stream.peek() { + while let Some(next_char_in_float) = self.peek_next() { match next_char_in_float { '0'..='9' | '_' => { result.push(next_char_in_float); @@ -1023,7 +1047,7 @@ impl<'a> TokenIterator<'a> { _ => panic!("unexpected character {}", ch), }); - while let Some(&next_char_in_hex) = self.stream.peek() { + while let Some(next_char_in_hex) = self.peek_next() { if !valid.contains(&next_char_in_hex) { break; } @@ -1079,7 +1103,7 @@ impl<'a> TokenIterator<'a> { let mut result = Vec::new(); result.push(c); - while let Some(&next_char) = self.stream.peek() { + while let Some(next_char) = self.peek_next() { match next_char { x if x.is_ascii_alphanumeric() || x == '_' => { result.push(x); @@ -1207,7 +1231,7 @@ impl<'a> TokenIterator<'a> { ('/', '/') => { self.eat_next(); - while let Some(c) = self.stream.next() { + while let Some(c) = self.get_next() { if c == '\n' { self.new_line(); break; @@ -1221,18 +1245,18 @@ impl<'a> TokenIterator<'a> { self.eat_next(); - while let Some(c) = self.stream.next() { + while let Some(c) = self.get_next() { self.advance(); match c { '/' => { - if self.stream.next() == Some('*') { + if self.get_next() == Some('*') { level += 1; } self.advance(); } '*' => { - if self.stream.next() == Some('/') { + if self.get_next() == Some('/') { level -= 1; } self.advance(); @@ -1272,7 +1296,7 @@ impl<'a> TokenIterator<'a> { self.eat_next(); return Some(( - if self.stream.peek() == Some(&'=') { + if self.peek_next() == Some('=') { self.eat_next(); Token::LeftShiftAssign } else { @@ -1291,7 +1315,7 @@ impl<'a> TokenIterator<'a> { self.eat_next(); return Some(( - if self.stream.peek() == Some(&'=') { + if self.peek_next() == Some('=') { self.eat_next(); Token::RightShiftAssign } else { @@ -1368,11 +1392,11 @@ impl<'a> Iterator for TokenIterator<'a> { } /// Tokenize an input text stream. -pub fn lex(input: &str) -> TokenIterator<'_> { +pub fn lex<'a>(input: &'a [&'a str]) -> TokenIterator<'a> { TokenIterator { can_be_unary: true, pos: Position::new(1, 0), - stream: input.chars().peekable(), + streams: input.iter().map(|s| s.chars().peekable()).collect(), } } @@ -2696,6 +2720,7 @@ pub fn parse_global_expr<'a, 'e>( input: &mut Peekable>, engine: &Engine<'e>, scope: &Scope, + optimization_level: OptimizationLevel, ) -> Result { let expr = parse_expr(input, false)?; @@ -2711,7 +2736,7 @@ pub fn parse_global_expr<'a, 'e>( scope, vec![Stmt::Expr(Box::new(expr))], vec![], - engine.optimization_level, + optimization_level, ), ) } diff --git a/tests/maps.rs b/tests/maps.rs index f6bc45db..2002d420 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -163,13 +163,9 @@ fn test_map_for() -> Result<(), EvalAltResult> { fn test_map_json() -> Result<(), EvalAltResult> { let engine = Engine::new(); - let mut scope = Scope::new(); - scope.push_constant("null", ()); - scope.push_constant("undefined", ()); - let json = r#"{"a":1, "b":true, "c":42, "$d e f!":"hello", "z":null}"#; - let map = engine.eval_expression_with_scope::(&mut scope, &("#".to_string() + json))?; + let map = engine.parse_json(json, true)?; assert!(!map.contains_key("x")); @@ -211,7 +207,7 @@ fn test_map_json() -> Result<(), EvalAltResult> { #[cfg(not(feature = "no_index"))] { - scope.clear(); + let mut scope = Scope::new(); scope.push_constant("map", map); assert_eq!( From 4b2cff715ebad08a10766d8726a3bbd498e9641b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 21:02:13 +0800 Subject: [PATCH 15/21] Optimize property access for object maps. --- src/optimize.rs | 46 ++++++++++++++++++++++++++++++++++++---------- src/parser.rs | 27 +++++++++++++++++++++++++++ src/result.rs | 8 ++++---- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/optimize.rs b/src/optimize.rs index fef42473..89825480 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -385,25 +385,51 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { // id = expr expr => Expr::Assignment(id, Box::new(optimize_expr(expr, state)), pos), }, + // lhs.rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(lhs, rhs, pos) => Expr::Dot( - Box::new(optimize_expr(*lhs, state)), - Box::new(optimize_expr(*rhs, state)), - pos, - ), + Expr::Dot(lhs, rhs, pos) => match (*lhs, *rhs) { + // map.string + (Expr::Map(items, pos), Expr::Property(s, _)) + if items.iter().all(|(_, x, _)| x.is_pure()) => + { + // Map literal where everything is pure - promote the indexed item. + // All other items can be thrown away. + state.set_dirty(); + items.into_iter().find(|(name, _, _)| name == s.as_ref()) + .map(|(_, expr, _)| expr.set_position(pos)) + .unwrap_or_else(|| Expr::Unit(pos)) + } + // lhs.rhs + (lhs, rhs) => Expr::Dot( + Box::new(optimize_expr(lhs, state)), + Box::new(optimize_expr(rhs, state)), + pos, + ) + } // lhs[rhs] #[cfg(not(feature = "no_index"))] Expr::Index(lhs, rhs, pos) => match (*lhs, *rhs) { // array[int] - (Expr::Array(mut items, _), Expr::IntegerConstant(i, _)) - if i >= 0 && (i as usize) < items.len() && items.iter().all(|x| x.is_pure()) => + (Expr::Array(mut items, pos), Expr::IntegerConstant(i, _)) + if i >= 0 && (i as usize) < items.len() && items.iter().all(Expr::is_pure) => { // 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) + items.remove(i as usize).set_position(pos) + } + // map[string] + (Expr::Map(items, pos), Expr::StringConstant(s, _)) + if items.iter().all(|(_, x, _)| x.is_pure()) => + { + // Map literal where everything is pure - promote the indexed item. + // All other items can be thrown away. + state.set_dirty(); + items.into_iter().find(|(name, _, _)| name == s.as_ref()) + .map(|(_, expr, _)| expr.set_position(pos)) + .unwrap_or_else(|| Expr::Unit(pos)) } // string[int] (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) @@ -580,11 +606,11 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), // constant-name - Expr::Variable(name, _) if state.contains_constant(&name) => { + Expr::Variable(name, pos) if state.contains_constant(&name) => { state.set_dirty(); // Replace constant with value - state.find_constant(&name).expect("should find constant in scope!").clone() + state.find_constant(&name).expect("should find constant in scope!").clone().set_position(pos) } // All other expressions - skip diff --git a/src/parser.rs b/src/parser.rs index 21f04e1f..d46acef5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -511,6 +511,33 @@ impl Expr { } } + /// Get the `Position` of the expression. + pub(crate) fn set_position(mut self, new_pos: Position) -> Self { + match &mut self { + Self::IntegerConstant(_, pos) + | Self::FloatConstant(_, pos) + | Self::CharConstant(_, pos) + | Self::StringConstant(_, pos) + | Self::Array(_, pos) + | Self::Map(_, pos) + | Self::Variable(_, pos) + | Self::Property(_, pos) + | Self::Stmt(_, pos) + | Self::FunctionCall(_, _, _, pos) + | Self::And(_, _, pos) + | Self::Or(_, _, pos) + | Self::In(_, _, pos) + | Self::True(pos) + | Self::False(pos) + | Self::Unit(pos) + | Self::Assignment(_, _, pos) + | Self::Dot(_, _, pos) + | Self::Index(_, _, pos) => *pos = new_pos, + } + + self + } + /// Is the expression pure? /// /// A pure expression has no side effects. diff --git a/src/result.rs b/src/result.rs index ce2b1b87..ddb56f0b 100644 --- a/src/result.rs +++ b/src/result.rs @@ -107,15 +107,15 @@ impl EvalAltResult { Self::ErrorArrayBounds(_, index, _) if *index < 0 => { "Array access expects non-negative index" } - Self::ErrorArrayBounds(0, _, _) => "Access of empty array", + Self::ErrorArrayBounds(0, _, _) => "Empty array has nothing to access", Self::ErrorArrayBounds(_, _, _) => "Array index out of bounds", Self::ErrorStringBounds(_, index, _) if *index < 0 => { "Indexing a string expects a non-negative index" } - Self::ErrorStringBounds(0, _, _) => "Indexing of empty string", + Self::ErrorStringBounds(0, _, _) => "Empty string has nothing to index", Self::ErrorStringBounds(_, _, _) => "String index out of bounds", - Self::ErrorLogicGuard(_) => "Boolean expression expected", - Self::ErrorFor(_) => "For loop expects array or range", + Self::ErrorLogicGuard(_) => "Boolean value expected", + Self::ErrorFor(_) => "For loop expects an array, object map, or range", Self::ErrorVariableNotFound(_, _) => "Variable not found", Self::ErrorAssignmentToUnknownLHS(_) => { "Assignment to an unsupported left-hand side expression" From 81894e52cb4efff9037f19ca9600fa83ca975be9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 21:02:38 +0800 Subject: [PATCH 16/21] Do not optimize single-use AST's. --- src/api.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/api.rs b/src/api.rs index 9b25a7cc..fde9b5d4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -689,7 +689,9 @@ impl<'e> Engine<'e> { scope: &mut Scope, script: &str, ) -> Result { - let ast = self.compile(script)?; + // Since the AST will be thrown away afterwards, don't bother to optimize it + let ast = + self.compile_with_scope_and_optimization_level(scope, script, OptimizationLevel::None)?; self.eval_ast_with_scope(scope, &ast) } @@ -734,8 +736,10 @@ impl<'e> Engine<'e> { scope: &mut Scope, script: &str, ) -> Result { - let ast = self.compile_expression(script)?; - + let scripts = [script]; + let stream = lex(&scripts); + // Since the AST will be thrown away afterwards, don't bother to optimize it + let ast = parse_global_expr(&mut stream.peekable(), self, scope, OptimizationLevel::None)?; self.eval_ast_with_scope(scope, &ast) } @@ -850,7 +854,9 @@ impl<'e> Engine<'e> { pub fn consume_with_scope(&self, scope: &mut Scope, script: &str) -> Result<(), EvalAltResult> { let scripts = [script]; let stream = lex(&scripts); - let ast = parse(&mut stream.peekable(), self, scope, self.optimization_level)?; + + // Since the AST will be thrown away afterwards, don't bother to optimize it + let ast = parse(&mut stream.peekable(), self, scope, OptimizationLevel::None)?; self.consume_ast_with_scope(scope, &ast) } From c1b16b1a35560748d620525142629eef97a406e2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 21:59:29 +0800 Subject: [PATCH 17/21] Add remove/insert to arrays and maps. --- README.md | 123 +++++++++++++++++++++++++++++------------------- src/builtin.rs | 35 ++++++++++---- tests/arrays.rs | 28 ++++++----- tests/maps.rs | 10 ++++ 4 files changed, 127 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index e4d6d354..14b205cf 100644 --- a/README.md +++ b/README.md @@ -1134,18 +1134,18 @@ record == "Bob X. Davis: age 42 ❤\n"; ### Built-in functions -The following standard functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on strings: +The following standard methods (defined in the standard library but excluded if using a [raw `Engine`]) operate on strings: -| Function | Description | -| ---------- | ------------------------------------------------------------------------ | -| `len` | returns the number of characters (not number of bytes) in the string | -| `pad` | pads the string with an character until a specified number of characters | -| `append` | Adds a character or a string to the end of another string | -| `clear` | empties the string | -| `truncate` | cuts off the string at exactly a specified number of characters | -| `contains` | checks if a certain character or sub-string occurs in the string | -| `replace` | replaces a substring with another | -| `trim` | trims the string | +| Function | Parameter(s) | Description | +| ---------- | ------------------------------------- | -------------------------------------------------------------------- | +| `len` | _none_ | returns the number of characters (not number of bytes) in the string | +| `pad` | character to pad, target length | pads the string with an character to a specified length | +| `append` | character/string to append | Adds a character or a string to the end of another string | +| `clear` | _none_ | empties the string | +| `truncate` | target length | cuts off the string at exactly a specified number of characters | +| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | +| `replace` | target sub-string, replacement string | replaces a substring with another | +| `trim` | _none_ | trims the string of whitespace at the beginning and end | ### Examples @@ -1193,30 +1193,49 @@ Arrays are disabled via the [`no_index`] feature. ### Built-in functions -The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on arrays: +The following methods (defined in the standard library but excluded if using a [raw `Engine`]) operate on arrays: -| Function | Description | -| ------------ | ------------------------------------------------------------------------------------- | -| `push` | inserts an element at the end | -| `append` | concatenates the second array to the end of the first | -| `+` operator | concatenates the first array with the second | -| `pop` | removes the last element and returns it ([`()`] if empty) | -| `shift` | removes the first element and returns it ([`()`] if empty) | -| `len` | returns the number of elements | -| `pad` | pads the array with an element until a specified length | -| `clear` | empties the array | -| `truncate` | cuts off the array at exactly a specified length (discarding all subsequent elements) | +| Function | Parameter(s) | Description | +| ------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `push` | element to insert | inserts an element at the end | +| `append` | array to append | concatenates the second array to the end of the first | +| `+` operator | first array, second array | concatenates the first array with the second | +| `insert` | element to insert, position
(beginning if <= 0, end if >= length) | insert an element at a certain index | +| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | +| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | +| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | +| `len` | _none_ | returns the number of elements | +| `pad` | element to pad, target length | pads the array with an element until a specified length | +| `clear` | _none_ | empties the array | +| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | ### Examples ```rust -let y = [1, 2, 3]; // array literal with 3 elements -y[1] = 42; +let y = [2, 3]; // array literal with 2 elements -print(1 in y); // use 'in' to test if an item exists in the array, prints true -print(9 in y); // ... prints false +y.insert(0, 1); // insert element at the beginning +y.insert(999, 4); // insert element at the end -print(y[1]); // prints 42 +y.len() == 4; + +y[0] == 1; +y[1] == 2; +y[2] == 3; +y[3] == 4; + +(1 in y) == true; // use 'in' to test if an item exists in the array +(42 in y) == false; + +y[1] = 42; // array elements can be reassigned + +(42 in y) == true; + +y.remove(2) == 3; // remove element + +y.len() == 3; + +y[2] == 4; // elements after the removed element are shifted ts.list = y; // arrays can be assigned completely (by value copy) let foo = ts.list[1]; @@ -1238,7 +1257,7 @@ foo == 1; y.push(4); // 4 elements y.push(5); // 5 elements -print(y.len()); // prints 5 +y.len() == 5; let first = y.shift(); // remove the first element, 4 elements remaining first == 1; @@ -1246,7 +1265,7 @@ first == 1; let last = y.pop(); // remove the last element, 3 elements remaining last == 5; -print(y.len()); // prints 3 +y.len() == 3; for item in y { // arrays can be iterated with a 'for' statement print(item); @@ -1254,15 +1273,15 @@ for item in y { // arrays can be iterated with a 'for' statement y.pad(10, "hello"); // pad the array up to 10 elements -print(y.len()); // prints 10 +y.len() == 10; y.truncate(5); // truncate the array to 5 elements -print(y.len()); // prints 5 +y.len() == 5; y.clear(); // empty the array -print(y.len()); // prints 0 +y.len() == 0; ``` `push` and `pad` are only defined for standard built-in types. For custom types, type-specific versions must be registered: @@ -1294,17 +1313,18 @@ Object maps are disabled via the [`no_object`] feature. ### Built-in functions -The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on object maps: +The following methods (defined in the standard library but excluded if using a [raw `Engine`]) operate on object maps: -| Function | Description | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `has` | does the object map contain a property of a particular name? | -| `len` | returns the number of properties | -| `clear` | empties the object map | -| `mixin` | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | -| `+` operator | merges the first object map with the second | -| `keys` | returns an [array] of all the property names (in random order) | -| `values` | returns an [array] of all the property values (in random order) | +| Function | Parameter(s) | Description | +| ------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `has` | property name | does the object map contain a property of a particular name? | +| `len` | _none_ | returns the number of properties | +| `clear` | _none_ | empties the object map | +| `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) | +| `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | +| `+` operator | first object map, second object map | merges the first object map with the second | +| `keys` | _none_ | returns an [array] of all the property names (in random order) | +| `values` | _none_ | returns an [array] of all the property values (in random order) | ### Examples @@ -1322,12 +1342,12 @@ y.a = 42; // access via dot notation y.baz!$@ = 42; // <- syntax error: only proper variable names allowed in dot notation y."baz!$@" = 42; // <- syntax error: strings not allowed in dot notation -print(y.a); // prints 42 +y.a == 42; -print(y["baz!$@"]); // prints 123.456 - access via index notation +y["baz!$@"] == 123.456; // access via index notation -print("baz!$@" in y); // use 'in' to test if a property exists in the object map, prints true -print("z" in y); // ... prints false +"baz!$@" in y == true; // use 'in' to test if a property exists in the object map, prints true +("z" in y) == false; ts.obj = y; // object maps can be assigned completely (by value copy) let foo = ts.list.a; @@ -1352,7 +1372,12 @@ y.has("xyz") == false; y.xyz == (); // a non-existing property returns '()' y["xyz"] == (); -print(y.len()); // prints 3 +y.len() == 3; + +y.remove("a") == 1; // remove property + +y.len() == 2; +y.has("a") == false; for name in keys(y) { // get an array of all the property names via the 'keys' function print(name); @@ -1364,7 +1389,7 @@ for val in values(y) { // get an array of all the property values via the 'valu y.clear(); // empty the object map -print(y.len()); // prints 0 +y.len() == 0; ``` ### Parsing from JSON diff --git a/src/builtin.rs b/src/builtin.rs index dfc709c2..e95dac04 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -620,15 +620,9 @@ impl Engine<'_> { #[cfg(not(feature = "no_object"))] { - self.register_fn(KEYWORD_PRINT, |x: &mut Map| -> String { - format!("#{:?}", x) - }); - self.register_fn(FUNC_TO_STRING, |x: &mut Map| -> String { - format!("#{:?}", x) - }); - self.register_fn(KEYWORD_DEBUG, |x: &mut Map| -> String { - format!("#{:?}", x) - }); + self.register_fn(KEYWORD_PRINT, |x: &mut Map| format!("#{:?}", x)); + self.register_fn(FUNC_TO_STRING, |x: &mut Map| format!("#{:?}", x)); + self.register_fn(KEYWORD_DEBUG, |x: &mut Map| format!("#{:?}", x)); // Register map access functions #[cfg(not(feature = "no_index"))] @@ -874,6 +868,15 @@ impl Engine<'_> { fn push(list: &mut Array, item: T) { list.push(Box::new(item)); } + fn ins(list: &mut Array, position: INT, item: T) { + if position <= 0 { + list.insert(0, Box::new(item)); + } else if (position as usize) >= list.len() - 1 { + push(list, item); + } else { + list.insert(position as usize, Box::new(item)); + } + } fn pad(list: &mut Array, len: INT, item: T) { if len >= 0 { while list.len() < len as usize { @@ -886,6 +889,7 @@ impl Engine<'_> { reg_fn2x!(self, "push", push, &mut Array, (), String, Array, ()); reg_fn3!(self, "pad", pad, &mut Array, INT, (), INT, bool, char); reg_fn3!(self, "pad", pad, &mut Array, INT, (), String, Array, ()); + reg_fn3!(self, "insert", ins, &mut Array, INT, (), String, Array, ()); self.register_fn("append", |list: &mut Array, array: Array| { list.extend(array) @@ -902,12 +906,15 @@ impl Engine<'_> { reg_fn2x!(self, "push", push, &mut Array, (), i32, i64, u32, u64); reg_fn3!(self, "pad", pad, &mut Array, INT, (), i8, u8, i16, u16); reg_fn3!(self, "pad", pad, &mut Array, INT, (), i32, u32, i64, u64); + reg_fn3!(self, "insert", ins, &mut Array, INT, (), i8, u8, i16, u16); + reg_fn3!(self, "insert", ins, &mut Array, INT, (), i32, i64, u32, u64); } #[cfg(not(feature = "no_float"))] { reg_fn2x!(self, "push", push, &mut Array, (), f32, f64); reg_fn3!(self, "pad", pad, &mut Array, INT, (), f32, f64); + reg_fn3!(self, "insert", ins, &mut Array, INT, (), f32, f64); } self.register_dynamic_fn("pop", |list: &mut Array| { @@ -920,6 +927,13 @@ impl Engine<'_> { list.remove(0) } }); + self.register_dynamic_fn("remove", |list: &mut Array, len: INT| { + if len < 0 || (len as usize) >= list.len() { + ().into_dynamic() + } else { + list.remove(len as usize) + } + }); self.register_fn("len", |list: &mut Array| list.len() as INT); self.register_fn("clear", |list: &mut Array| list.clear()); self.register_fn("truncate", |list: &mut Array, len: INT| { @@ -935,6 +949,9 @@ impl Engine<'_> { self.register_fn("has", |map: &mut Map, prop: String| map.contains_key(&prop)); self.register_fn("len", |map: &mut Map| map.len() as INT); self.register_fn("clear", |map: &mut Map| map.clear()); + self.register_dynamic_fn("remove", |x: &mut Map, name: String| { + x.remove(&name).unwrap_or(().into_dynamic()) + }); self.register_fn("mixin", |map1: &mut Map, map2: Map| { map2.into_iter().for_each(|(key, value)| { map1.insert(key, value); diff --git a/tests/arrays.rs b/tests/arrays.rs index 33568460..a41f0a63 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -16,20 +16,26 @@ fn test_arrays() -> Result<(), EvalAltResult> { assert_eq!( engine.eval::( r" - let x = [1, 2, 3]; - let y = [4, 5]; - x.append(y); - x.len() + let x = [2, 9]; + x.insert(-1, 1); + x.insert(999, 3); + + let r = x.remove(2); + + let y = [4, 5]; + x.append(y); + + x.len() + r " )?, - 5 + 14 ); assert_eq!( engine.eval::( r" - let x = [1, 2, 3]; - x += [4, 5]; - x.len() + let x = [1, 2, 3]; + x += [4, 5]; + x.len() " )?, 5 @@ -38,9 +44,9 @@ fn test_arrays() -> Result<(), EvalAltResult> { engine .eval::( r" - let x = [1, 2, 3]; - let y = [4, 5]; - x + y + let x = [1, 2, 3]; + let y = [4, 5]; + x + y " )? .len(), diff --git a/tests/maps.rs b/tests/maps.rs index 2002d420..ac591966 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -33,6 +33,16 @@ fn test_map_indexing() -> Result<(), EvalAltResult> { assert!(engine.eval::("let y = #{a: 1, b: 2, c: 3}; 'b' in y")?); assert!(!engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "z" in y"#)?); + assert_eq!( + engine.eval::( + r#" + let x = #{a: 1, b: 2, c: 3}; + let c = x.remove("c"); + x.len() + c + "# + )?, + 5 + ); assert_eq!( engine.eval::( r" From d73cfb6da587a0601503653faca8fe8d82c75192 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 11 Apr 2020 12:46:10 +0800 Subject: [PATCH 18/21] Allow escaping quotes in strings. --- README.md | 16 +++++++++++++++- src/parser.rs | 5 ++++- tests/chars.rs | 2 ++ tests/string.rs | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14b205cf..b39b3777 100644 --- a/README.md +++ b/README.md @@ -1076,7 +1076,21 @@ hex ('`\x`_xx_') escape sequences. Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicode code points and '`\U`' maps the full, 32-bit extended Unicode code points. -Internally Rhai strings are stored as UTF-8 just like Rust (they _are_ Rust `String`s!), but there are major differences. +Standard escape sequences: + +| Escape sequence | Meaning | +| --------------- | ------------------------------ | +| `\\` | back-slash `\` | +| `\t` | tab | +| `\r` | carriage-return `CR` | +| `\n` | line-feed `LF` | +| `\"` | double-quote `"` in strings | +| `\'` | single-quote `'` in characters | +| `\x`_xx_ | Unicode in 2-digit hex | +| `\u`_xxxx_ | Unicode in 4-digit hex | +| `\U`_xxxxxxxx_ | Unicode in 8-digit hex | + +Internally Rhai strings are stored as UTF-8 just like Rust (they _are_ Rust `String`'s!), but there are major differences. In Rhai a string is the same as an array of Unicode characters and can be directly indexed (unlike Rust). This is similar to most other languages where strings are internally represented not as UTF-8 but as arrays of multi-byte Unicode characters. diff --git a/src/parser.rs b/src/parser.rs index d46acef5..b34a2f09 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -978,7 +978,10 @@ impl<'a> TokenIterator<'a> { } // \{enclosing_char} - escaped - ch if enclosing_char == ch && !escape.is_empty() => result.push(ch), + ch if enclosing_char == ch && !escape.is_empty() => { + escape.clear(); + result.push(ch) + } // Close wrapper ch if enclosing_char == ch && escape.is_empty() => break, diff --git a/tests/chars.rs b/tests/chars.rs index 8d6be4d8..db13f840 100644 --- a/tests/chars.rs +++ b/tests/chars.rs @@ -5,6 +5,8 @@ fn test_chars() -> Result<(), EvalAltResult> { let engine = Engine::new(); assert_eq!(engine.eval::("'y'")?, 'y'); + assert_eq!(engine.eval::(r"'\''")?, '\''); + assert_eq!(engine.eval::(r#"'"'"#)?, '"'); assert_eq!(engine.eval::("'\\u2764'")?, '❤'); #[cfg(not(feature = "no_index"))] diff --git a/tests/string.rs b/tests/string.rs index 3eeb3a35..5e60d87e 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -12,6 +12,7 @@ fn test_string() -> Result<(), EvalAltResult> { engine.eval::(r#""Test string: \x58""#)?, "Test string: X" ); + assert_eq!(engine.eval::(r#""\"hello\"""#)?, r#""hello""#); assert_eq!(engine.eval::(r#""foo" + "bar""#)?, "foobar"); From 5848339d5a73f0162e363a08ce39012685905d2c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 11 Apr 2020 16:06:57 +0800 Subject: [PATCH 19/21] Add timestamp support. --- README.md | 31 ++++++++ scripts/primes.rhai | 3 + scripts/speed_test.rhai | 3 +- src/builtin.rs | 93 ++++++++++++++++-------- src/engine.rs | 156 +++++++++++++++++++++------------------- tests/string.rs | 1 + tests/time.rs | 39 ++++++++++ 7 files changed, 223 insertions(+), 103 deletions(-) create mode 100644 tests/time.rs diff --git a/README.md b/README.md index b39b3777..c073d244 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,7 @@ The following primitive types are supported natively: | **Unicode string** | `String` (_not_ `&str`) | `"string"` | `"hello"` etc. | | **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ? ? ? ]"` | | **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `#{ "a": 1, "b": 2 }` | +| **Timestamp** (implemented in standard library) | `std::time::Instant` | `"timestamp"` | _not supported_ | | **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | | **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | | **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | @@ -1448,6 +1449,36 @@ let result = engine.eval_with_scope::(r#"map["^^^!!!"].len()"#)?; result == 3; // the object map is successfully used in the script ``` +`timestamp`'s +------------- +[`timestamp`]: #timestamp-s + +Timestamps are provided by the standard library (excluded if using a [raw `Engine`]) via the `timestamp` +function. + +The Rust type of a timestamp is `std::time::Instant`. [`type_of()`] a timestamp returns `"timestamp"`. + +### Built-in functions + +The following methods (defined in the standard library but excluded if using a [raw `Engine`]) operate on timestamps: + +| Function | Parameter(s) | Description | +| ------------ | ---------------------------------- | -------------------------------------------------------- | +| `elapsed` | _none_ | returns the number of seconds since the timestamp | +| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | + +### Examples + +```rust +let now = timestamp(); + +// Do some lengthy operation... + +if now.elapsed() > 30.0 { + print("takes too long (over 30 seconds)!") +} +``` + Comparison operators -------------------- diff --git a/scripts/primes.rhai b/scripts/primes.rhai index d09418e8..6d5a49d5 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -1,5 +1,7 @@ // This script uses the Sieve of Eratosthenes to calculate prime numbers. +let now = timestamp(); + const MAX_NUMBER_TO_CHECK = 10_000; // 1229 primes <= 10000 let prime_mask = []; @@ -24,3 +26,4 @@ for p in range(2, MAX_NUMBER_TO_CHECK) { } print("Total " + total_primes_found + " primes."); +print("Run time = " + now.elapsed() + " seconds."); diff --git a/scripts/speed_test.rhai b/scripts/speed_test.rhai index 1aa67276..5709d6ac 100644 --- a/scripts/speed_test.rhai +++ b/scripts/speed_test.rhai @@ -1,6 +1,7 @@ // This script runs 1 million iterations // to test the speed of the scripting engine. +let now = timestamp(); let x = 1_000_000; print("Ready... Go!"); @@ -9,4 +10,4 @@ while x > 0 { x = x - 1; } -print("Finished."); +print("Finished. Run time = " + now.elapsed() + " seconds."); diff --git a/src/builtin.rs b/src/builtin.rs index e95dac04..f77b4f4a 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -27,6 +27,7 @@ use crate::stdlib::{ format, ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Range, Rem, Shl, Shr, Sub}, string::{String, ToString}, + time::Instant, vec::Vec, {i32, i64, u32}, }; @@ -57,6 +58,34 @@ macro_rules! reg_op_result1 { ) } +macro_rules! reg_cmp { + ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( + $( + $self.register_fn($x, $op as fn(x: $y, y: $y)->bool); + )* + ) +} + +// Comparison operators +fn lt(x: T, y: T) -> bool { + x < y +} +fn lte(x: T, y: T) -> bool { + x <= y +} +fn gt(x: T, y: T) -> bool { + x > y +} +fn gte(x: T, y: T) -> bool { + x >= y +} +fn eq(x: T, y: T) -> bool { + x == y +} +fn ne(x: T, y: T) -> bool { + x != y +} + impl Engine<'_> { /// Register the core built-in library. pub(crate) fn register_core_lib(&mut self) { @@ -176,26 +205,6 @@ impl Engine<'_> { } } - // Comparison operators - fn lt(x: T, y: T) -> bool { - x < y - } - fn lte(x: T, y: T) -> bool { - x <= y - } - fn gt(x: T, y: T) -> bool { - x > y - } - fn gte(x: T, y: T) -> bool { - x >= y - } - fn eq(x: T, y: T) -> bool { - x == y - } - fn ne(x: T, y: T) -> bool { - x != y - } - // Logic operators fn and(x: bool, y: bool) -> bool { x && y @@ -395,14 +404,6 @@ impl Engine<'_> { } { - macro_rules! reg_cmp { - ($self:expr, $x:expr, $op:expr, $( $y:ty ),*) => ( - $( - $self.register_fn($x, $op as fn(x: $y, y: $y)->bool); - )* - ) - } - reg_cmp!(self, "<", lt, INT, String, char); reg_cmp!(self, "<=", lte, INT, String, char); reg_cmp!(self, ">", gt, INT, String, char); @@ -433,7 +434,7 @@ impl Engine<'_> { } // `&&` and `||` are treated specially as they short-circuit. - // They are implemented as special `Expr` instances, not function calls. + // They are implemented as special `Expr` Instants, not function calls. //reg_op!(self, "||", or, bool); //reg_op!(self, "&&", and, bool); @@ -1031,5 +1032,39 @@ impl Engine<'_> { *s = trimmed.to_string(); } }); + + // Register date/time functions + self.register_fn("timestamp", || Instant::now()); + + self.register_fn("-", |ts1: Instant, ts2: Instant| { + if ts2 > ts1 { + #[cfg(not(feature = "no_float"))] + return -(ts2 - ts1).as_secs_f64(); + + #[cfg(feature = "no_float")] + return -((ts2 - ts1).as_secs() as INT); + } else { + #[cfg(not(feature = "no_float"))] + return (ts1 - ts2).as_secs_f64(); + + #[cfg(feature = "no_float")] + return (ts1 - ts2).as_secs() as INT; + } + }); + + reg_cmp!(self, "<", lt, Instant); + reg_cmp!(self, "<=", lte, Instant); + reg_cmp!(self, ">", gt, Instant); + reg_cmp!(self, ">=", gte, Instant); + reg_cmp!(self, "==", eq, Instant); + reg_cmp!(self, "!=", ne, Instant); + + self.register_fn("elapsed", |timestamp: Instant| { + #[cfg(not(feature = "no_float"))] + return timestamp.elapsed().as_secs_f64(); + + #[cfg(feature = "no_float")] + return timestamp.elapsed().as_secs() as INT; + }); } } diff --git a/src/engine.rs b/src/engine.rs index 5abe17eb..3d7f3379 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -19,6 +19,7 @@ use crate::stdlib::{ rc::Rc, string::{String, ToString}, sync::Arc, + time::Instant, vec, vec::Vec, }; @@ -362,6 +363,7 @@ impl Engine<'_> { #[cfg(not(feature = "no_object"))] (type_name::(), "map"), (type_name::(), "string"), + (type_name::(), "timestamp"), (type_name::(), "dynamic"), (type_name::(), "variant"), ] @@ -719,23 +721,24 @@ impl Engine<'_> { let value = self.get_dot_val_helper(scope, fn_lib, target, dot_rhs, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. - if let Some(src) = src { - match src.typ { - ScopeEntryType::Constant => { - return Err(EvalAltResult::ErrorAssignmentToConstant( - src.name.to_string(), - idx_lhs.position(), - )); - } - ScopeEntryType::Normal => { - Self::update_indexed_var_in_scope( - idx_src_type, - scope, - src, - index, - (val, dot_rhs.position()), - )?; - } + match src.map(|s| s.typ) { + None => (), + + Some(ScopeEntryType::Constant) => { + return Err(EvalAltResult::ErrorAssignmentToConstant( + src.unwrap().name.to_string(), + idx_lhs.position(), + )); + } + + Some(ScopeEntryType::Normal) => { + Self::update_indexed_var_in_scope( + idx_src_type, + scope, + src.unwrap(), + index, + (val, dot_rhs.position()), + )?; } } @@ -1106,16 +1109,16 @@ impl Engine<'_> { match dot_lhs { // id.??? Expr::Variable(id, pos) => { - let (entry, mut target) = Self::search_scope(scope, id, *pos)?; + let (src, mut target) = Self::search_scope(scope, id, *pos)?; - match entry.typ { + match src.typ { ScopeEntryType::Constant => Err(EvalAltResult::ErrorAssignmentToConstant( id.to_string(), op_pos, )), _ => { // Avoid referencing scope which is used below as mut - let entry = ScopeSource { name: id, ..entry }; + let entry = ScopeSource { name: id, ..src }; let this_ptr = target.as_mut(); let value = self .set_dot_val_helper(scope, fn_lib, this_ptr, dot_rhs, new_val, level); @@ -1139,23 +1142,24 @@ impl Engine<'_> { self.set_dot_val_helper(scope, fn_lib, this_ptr, dot_rhs, new_val, level); // In case the expression mutated `target`, we need to update it back into the scope because it is cloned. - if let Some(src) = src { - match src.typ { - ScopeEntryType::Constant => { - return Err(EvalAltResult::ErrorAssignmentToConstant( - src.name.to_string(), - lhs.position(), - )); - } - ScopeEntryType::Normal => { - Self::update_indexed_var_in_scope( - idx_src_type, - scope, - src, - index, - (target, val_pos), - )?; - } + match src.map(|x| x.typ) { + None => (), + + Some(ScopeEntryType::Constant) => { + return Err(EvalAltResult::ErrorAssignmentToConstant( + src.unwrap().name.to_string(), + lhs.position(), + )); + } + + Some(ScopeEntryType::Normal) => { + Self::update_indexed_var_in_scope( + idx_src_type, + scope, + src.unwrap(), + index, + (target, val_pos), + )?; } } @@ -1260,29 +1264,36 @@ impl Engine<'_> { match lhs.as_ref() { // name = rhs - Expr::Variable(name, pos) => match scope - .get(name) - .ok_or_else(|| { - EvalAltResult::ErrorVariableNotFound(name.clone().into_owned(), *pos) - })? - .0 - { - entry - @ - ScopeSource { - typ: ScopeEntryType::Normal, - .. - } => { + Expr::Variable(name, pos) => match scope.get(name) { + None => { + return Err(EvalAltResult::ErrorVariableNotFound( + name.clone().into_owned(), + *pos, + )) + } + + Some(( + entry + @ + ScopeSource { + typ: ScopeEntryType::Normal, + .. + }, + _, + )) => { // Avoid referencing scope which is used below as mut let entry = ScopeSource { name, ..entry }; *scope.get_mut(entry) = rhs_val.clone(); Ok(rhs_val) } - ScopeSource { - typ: ScopeEntryType::Constant, - .. - } => Err(EvalAltResult::ErrorAssignmentToConstant( + Some(( + ScopeSource { + typ: ScopeEntryType::Constant, + .. + }, + _, + )) => Err(EvalAltResult::ErrorAssignmentToConstant( name.to_string(), *op_pos, )), @@ -1294,26 +1305,25 @@ impl Engine<'_> { let (idx_src_type, src, index, _) = self.eval_index_expr(scope, fn_lib, idx_lhs, idx_expr, *op_pos, level)?; - if let Some(src) = src { - match src.typ { - ScopeEntryType::Constant => { - Err(EvalAltResult::ErrorAssignmentToConstant( - src.name.to_string(), - idx_lhs.position(), - )) - } - ScopeEntryType::Normal => Ok(Self::update_indexed_var_in_scope( - idx_src_type, - scope, - src, - index, - (rhs_val, rhs.position()), - )?), - } - } else { - Err(EvalAltResult::ErrorAssignmentToUnknownLHS( + match src.map(|x| x.typ) { + None => Err(EvalAltResult::ErrorAssignmentToUnknownLHS( idx_lhs.position(), - )) + )), + + Some(ScopeEntryType::Constant) => { + Err(EvalAltResult::ErrorAssignmentToConstant( + src.unwrap().name.to_string(), + idx_lhs.position(), + )) + } + + Some(ScopeEntryType::Normal) => Ok(Self::update_indexed_var_in_scope( + idx_src_type, + scope, + src.unwrap(), + index, + (rhs_val, rhs.position()), + )?), } } diff --git a/tests/string.rs b/tests/string.rs index 5e60d87e..7454dcf2 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -24,6 +24,7 @@ fn test_string() -> Result<(), EvalAltResult> { assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); #[cfg(not(feature = "no_stdlib"))] + #[cfg(not(feature = "no_object"))] assert_eq!(engine.eval::("(42).to_string()")?, "42"); #[cfg(not(feature = "no_float"))] diff --git a/tests/time.rs b/tests/time.rs new file mode 100644 index 00000000..10b232fe --- /dev/null +++ b/tests/time.rs @@ -0,0 +1,39 @@ +#![cfg(not(feature = "no_stdlib"))] + +use rhai::{Engine, EvalAltResult, INT}; + +#[cfg(not(feature = "no_float"))] +use rhai::FLOAT; + +#[test] +fn test_timestamp() -> Result<(), EvalAltResult> { + let engine = Engine::new(); + + assert_eq!(engine.eval::("type_of(timestamp())")?, "timestamp"); + + #[cfg(not(feature = "no_float"))] + assert!( + engine.eval::( + r#" + let time = timestamp(); + let x = 10_000; + while x > 0 { x -= 1; } + elapsed(time) + "# + )? < 10.0 + ); + + #[cfg(feature = "no_float")] + assert!( + engine.eval::( + r#" + let time = timestamp(); + let x = 10_000; + while x > 0 { x -= 1; } + elapsed(time) + "# + )? < 10 + ); + + Ok(()) +} From bc0d43d68f27f772c9e6859d796688ffbc772849 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 11 Apr 2020 18:09:03 +0800 Subject: [PATCH 20/21] Simplify code. --- src/engine.rs | 187 ++++++++++++++++++++++-------------------------- src/optimize.rs | 22 +++--- src/parser.rs | 9 ++- 3 files changed, 102 insertions(+), 116 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 3d7f3379..73b1e800 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -172,11 +172,10 @@ impl FunctionsLib { } /// Get a function definition from the `FunctionsLib`. pub fn get_function(&self, name: &str, params: usize) -> Option<&FnDef> { - if let Ok(n) = self.0.binary_search_by(|f| f.compare(name, params)) { - Some(&self.0[n]) - } else { - None - } + self.0 + .binary_search_by(|f| f.compare(name, params)) + .ok() + .map(|n| self.0[n].as_ref()) } /// Merge another `FunctionsLib` into this `FunctionsLib`. pub fn merge(&self, other: &Self) -> Self { @@ -441,58 +440,56 @@ impl Engine<'_> { const fn_lib: Option<&FunctionsLib> = None; // First search in script-defined functions (can override built-in) - if let Some(lib) = fn_lib { - if let Some(fn_def) = lib.get_function(fn_name, args.len()) { - match scope { - // Extern scope passed in which is not empty - Some(scope) if scope.len() > 0 => { - let scope_len = scope.len(); + if let Some(fn_def) = fn_lib.and_then(|lib| lib.get_function(fn_name, args.len())) { + match scope { + // Extern scope passed in which is not empty + Some(scope) if scope.len() > 0 => { + let scope_len = scope.len(); - scope.extend( - // Put arguments into scope as variables - variable name is copied - // TODO - avoid copying variable name - fn_def - .params - .iter() - .zip(args.into_iter().map(|x| (*x).into_dynamic())) - .map(|(name, value)| (name.clone(), ScopeEntryType::Normal, value)), - ); + scope.extend( + // Put arguments into scope as variables - variable name is copied + // TODO - avoid copying variable name + fn_def + .params + .iter() + .zip(args.into_iter().map(|x| (*x).into_dynamic())) + .map(|(name, value)| (name.clone(), ScopeEntryType::Normal, value)), + ); - // Evaluate the function at one higher level of call depth - let result = self - .eval_stmt(scope, fn_lib, &fn_def.body, level + 1) - .or_else(|err| match err { - // Convert return statement to return value - EvalAltResult::Return(x, _) => Ok(x), - err => Err(err.set_position(pos)), - }); + // Evaluate the function at one higher level of call depth + let result = self + .eval_stmt(scope, fn_lib, &fn_def.body, level + 1) + .or_else(|err| match err { + // Convert return statement to return value + EvalAltResult::Return(x, _) => Ok(x), + err => Err(err.set_position(pos)), + }); - scope.rewind(scope_len); + scope.rewind(scope_len); - return result; - } - // No new scope - create internal scope - _ => { - let mut scope = Scope::new(); + return result; + } + // No new scope - create internal scope + _ => { + let mut scope = Scope::new(); - scope.extend( - // Put arguments into scope as variables - fn_def - .params - .iter() - .zip(args.into_iter().map(|x| (*x).into_dynamic())) - .map(|(name, value)| (name, ScopeEntryType::Normal, value)), - ); + scope.extend( + // Put arguments into scope as variables + fn_def + .params + .iter() + .zip(args.into_iter().map(|x| (*x).into_dynamic())) + .map(|(name, value)| (name, ScopeEntryType::Normal, value)), + ); - // Evaluate the function at one higher level of call depth - return self - .eval_stmt(&mut scope, fn_lib, &fn_def.body, level + 1) - .or_else(|err| match err { - // Convert return statement to return value - EvalAltResult::Return(x, _) => Ok(x), - err => Err(err.set_position(pos)), - }); - } + // Evaluate the function at one higher level of call depth + return self + .eval_stmt(&mut scope, fn_lib, &fn_def.body, level + 1) + .or_else(|err| match err { + // Convert return statement to return value + EvalAltResult::Return(x, _) => Ok(x), + err => Err(err.set_position(pos)), + }); } } } @@ -510,25 +507,23 @@ impl Engine<'_> { } // Search built-in's and external functions - if let Some(functions) = &self.functions { - if let Some(func) = functions.get(&spec) { - // Run external function - let result = func(args, pos)?; + if let Some(func) = self.functions.as_ref().and_then(|f| f.get(&spec)) { + // Run external function + let result = func(args, pos)?; - // See if the function match print/debug (which requires special processing) - return Ok(match fn_name { - KEYWORD_PRINT if self.on_print.is_some() => { - self.on_print.as_ref().unwrap()(cast_to_string(result.as_ref(), pos)?) - .into_dynamic() - } - KEYWORD_DEBUG if self.on_debug.is_some() => { - self.on_debug.as_ref().unwrap()(cast_to_string(result.as_ref(), pos)?) - .into_dynamic() - } - KEYWORD_PRINT | KEYWORD_DEBUG => ().into_dynamic(), - _ => result, - }); - } + // See if the function match print/debug (which requires special processing) + return match fn_name { + KEYWORD_PRINT if self.on_print.is_some() => Ok(self.on_print.as_ref().unwrap()( + cast_to_string(result.as_ref(), pos)?, + ) + .into_dynamic()), + KEYWORD_DEBUG if self.on_debug.is_some() => Ok(self.on_debug.as_ref().unwrap()( + cast_to_string(result.as_ref(), pos)?, + ) + .into_dynamic()), + KEYWORD_PRINT | KEYWORD_DEBUG => Ok(().into_dynamic()), + _ => Ok(result), + }; } if let Some(prop) = extract_prop_from_getter(fn_name) { @@ -1603,33 +1598,29 @@ impl Engine<'_> { let arr = self.eval_expr(scope, fn_lib, expr, level)?; let tid = Any::type_id(arr.as_ref()); - if let Some(type_iterators) = &self.type_iterators { - if let Some(iter_fn) = type_iterators.get(&tid) { - // Add the loop variable - variable name is copied - // TODO - avoid copying variable name - scope.push(name.clone(), ()); + if let Some(iter_fn) = self.type_iterators.as_ref().and_then(|t| t.get(&tid)) { + // Add the loop variable - variable name is copied + // TODO - avoid copying variable name + scope.push(name.clone(), ()); - let entry = ScopeSource { - name, - index: scope.len() - 1, - typ: ScopeEntryType::Normal, - }; + let entry = ScopeSource { + name, + index: scope.len() - 1, + typ: ScopeEntryType::Normal, + }; - for a in iter_fn(&arr) { - *scope.get_mut(entry) = a; + for a in iter_fn(&arr) { + *scope.get_mut(entry) = a; - match self.eval_stmt(scope, fn_lib, body, level) { - Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), - Err(EvalAltResult::ErrorLoopBreak(true, _)) => break, - Err(x) => return Err(x), - } + match self.eval_stmt(scope, fn_lib, body, level) { + Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), + Err(EvalAltResult::ErrorLoopBreak(true, _)) => break, + Err(x) => return Err(x), } - - scope.rewind(scope.len() - 1); - Ok(().into_dynamic()) - } else { - Err(EvalAltResult::ErrorFor(expr.position())) } + + scope.rewind(scope.len() - 1); + Ok(().into_dynamic()) } else { Err(EvalAltResult::ErrorFor(expr.position())) } @@ -1694,16 +1685,10 @@ impl Engine<'_> { /// Map a type_name into a pretty-print name pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { - if self.type_names.is_none() { - name - } else { - self.type_names - .as_ref() - .unwrap() - .get(name) - .map(String::as_str) - .unwrap_or(name) - } + self.type_names + .as_ref() + .and_then(|list| list.get(name).map(String::as_str)) + .unwrap_or(name) } } diff --git a/src/optimize.rs b/src/optimize.rs index 89825480..fc5a4577 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -663,16 +663,18 @@ fn optimize<'a>( .into_iter() .enumerate() .map(|(i, stmt)| { - if let Stmt::Const(name, value, _) = &stmt { - // Load constants - state.push_constant(name, value.as_ref().clone()); - stmt // Keep it in the global scope - } else { - // Keep all variable declarations at this level - // and always keep the last return value - let keep = matches!(stmt, Stmt::Let(_, _, _)) || i == num_statements - 1; - - optimize_stmt(stmt, &mut state, keep) + match stmt { + Stmt::Const(ref name, ref value, _) => { + // Load constants + state.push_constant(name.as_ref(), value.as_ref().clone()); + stmt // Keep it in the global scope + } + _ => { + // Keep all variable declarations at this level + // and always keep the last return value + let keep = matches!(stmt, Stmt::Let(_, _, _)) || i == num_statements - 1; + optimize_stmt(stmt, &mut state, keep) + } } }) .collect(); diff --git a/src/parser.rs b/src/parser.rs index b34a2f09..9874edcf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2192,11 +2192,10 @@ fn parse_binary_op<'a>( let mut current_lhs = lhs; loop { - let (current_precedence, bind_right) = if let Some((current_op, _)) = input.peek() { - (current_op.precedence(), current_op.is_bind_right()) - } else { - (0, false) - }; + let (current_precedence, bind_right) = input.peek().map_or_else( + || (0, false), + |(current_op, _)| (current_op.precedence(), current_op.is_bind_right()), + ); // Bind left to the parent lhs expression if precedence is higher // If same precedence, then check if the operator binds right From 50a0f14bfc0a804834598cb6b2fe5b07f8980da3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 12 Apr 2020 21:24:30 +0800 Subject: [PATCH 21/21] Use version numbers in Cargo.toml --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 29511043..87cd6436 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ keywords = [ "scripting" ] categories = [ "no-std", "embedded", "parser-implementations" ] [dependencies] -num-traits = "*" +num-traits = { version = "0.2.11", default-features = false } [features] #default = ["no_stdlib", "no_function", "no_index", "no_object", "no_float", "only_i32", "unchecked", "no_optimize", "sync"] @@ -46,21 +46,21 @@ codegen-units = 1 #panic = 'abort' # remove stack backtrace for no-std [dependencies.libm] -version = "*" +version = "0.2.1" optional = true [dependencies.core-error] -version = "*" +version = "0.0.0" features = ["alloc"] optional = true [dependencies.hashbrown] -version = "*" +version = "0.7.1" default-features = false features = ["ahash", "nightly", "inline-more"] optional = true [dependencies.ahash] -version = "*" +version = "0.3.2" default-features = false optional = true