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");