diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f91b71a..4a47c966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ Deprecated API's * `FnPtr::num_curried` is deprecated in favor of `FnPtr::curry().len()`. +New features +------------ + +* The _Elvis operator_ (`?.`) is now supported for property access and method calls. + Enhancements ------------ diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 75d70dbc..e7fdd3fd 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -400,18 +400,18 @@ pub enum Expr { Stmt(Box), /// func `(` expr `,` ... `)` FnCall(Box, Position), - /// lhs `.` rhs + /// lhs `.` rhs | lhs `?.` rhs /// /// ### Flags /// - /// No flags are defined at this time. Use [`NONE`][ASTFlags::NONE]. + /// [`NEGATED`][ASTFlags::NEGATED] = `?.` (`.` if unset) + /// [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset) Dot(Box, ASTFlags, Position), /// lhs `[` rhs `]` /// /// ### Flags /// - /// [`NONE`][ASTFlags::NONE] = recurse into the indexing chain - /// [`BREAK`][ASTFlags::BREAK] = terminate the indexing chain + /// [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset) Index(Box, ASTFlags, Position), /// lhs `&&` rhs And(Box, Position), @@ -484,26 +484,37 @@ impl fmt::Debug for Expr { f.debug_list().entries(x.iter()).finish() } Self::FnCall(x, ..) => fmt::Debug::fmt(x, f), - Self::Index(x, term, pos) => { + Self::Index(x, options, pos) => { if !pos.is_none() { display_pos = format!(" @ {:?}", pos); } - f.debug_struct("Index") - .field("lhs", &x.lhs) - .field("rhs", &x.rhs) - .field("terminate", term) - .finish() + let mut f = f.debug_struct("Index"); + + f.field("lhs", &x.lhs).field("rhs", &x.rhs); + if !options.is_empty() { + f.field("options", options); + } + f.finish() } - Self::Dot(x, _, pos) | Self::And(x, pos) | Self::Or(x, pos) => { + Self::Dot(x, options, pos) => { + if !pos.is_none() { + display_pos = format!(" @ {:?}", pos); + } + + let mut f = f.debug_struct("Dot"); + + f.field("lhs", &x.lhs).field("rhs", &x.rhs); + if !options.is_empty() { + f.field("options", options); + } + f.finish() + } + Self::And(x, pos) | Self::Or(x, pos) => { let op_name = match self { - Self::Dot(..) => "Dot", Self::And(..) => "And", Self::Or(..) => "Or", - expr => unreachable!( - "Self::Dot or Self::And or Self::Or expected but gets {:?}", - expr - ), + expr => unreachable!("Self::And or Self::Or expected but gets {:?}", expr), }; if !pos.is_none() { @@ -802,7 +813,7 @@ impl Expr { pub const fn is_valid_postfix(&self, token: &Token) -> bool { match token { #[cfg(not(feature = "no_object"))] - Token::Period => return true, + Token::Period | Token::Elvis => return true, #[cfg(not(feature = "no_index"))] Token::LeftBracket => return true, _ => (), diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index ba946764..af62ebe5 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -186,6 +186,11 @@ impl Engine { #[cfg(not(feature = "no_object"))] ChainType::Dotting => { + // Check for existence with the Elvis operator + if _parent_options.contains(ASTFlags::NEGATED) && target.is::<()>() { + return Ok((Dynamic::UNIT, false)); + } + match rhs { // xxx.fn_name(arg_expr_list) Expr::MethodCall(x, pos) if !x.is_qualified() && new_val.is_none() => { diff --git a/src/optimizer.rs b/src/optimizer.rs index bf5f5e1e..ac4a811e 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -912,9 +912,15 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) { _ => () } } + // ()?.rhs + #[cfg(not(feature = "no_object"))] + Expr::Dot(x, options, ..) if options.contains(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => { + state.set_dirty(); + *expr = mem::take(&mut x.lhs); + } // lhs.rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(x,_, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) { + Expr::Dot(x, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) { // map.string (Expr::Map(m, pos), Expr::Property(p, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => { let prop = p.2.as_str(); @@ -932,7 +938,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) { } // ....lhs.rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(x,_, ..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); } + Expr::Dot(x,..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); } // lhs[rhs] #[cfg(not(feature = "no_index"))] diff --git a/src/parser.rs b/src/parser.rs index 0dfdcf4b..e7d63458 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1639,7 +1639,7 @@ impl Engine { } // Property access #[cfg(not(feature = "no_object"))] - (expr, Token::Period) => { + (expr, op @ Token::Period) | (expr, op @ Token::Elvis) => { // Expression after dot must start with an identifier match input.peek().expect(NEVER_ENDS) { (Token::Identifier(..), ..) => { @@ -1654,7 +1654,12 @@ impl Engine { } let rhs = self.parse_primary(input, state, lib, settings.level_up())?; - Self::make_dot_expr(state, expr, ASTFlags::NONE, rhs, tail_pos)? + let op_flags = match op { + Token::Period => ASTFlags::NONE, + Token::Elvis => ASTFlags::NEGATED, + _ => unreachable!(), + }; + Self::make_dot_expr(state, expr, rhs, ASTFlags::NONE, op_flags, tail_pos)? } // Unknown postfix operator (expr, token) => unreachable!( @@ -1959,14 +1964,22 @@ impl Engine { fn make_dot_expr( state: &mut ParseState, lhs: Expr, - parent_options: ASTFlags, rhs: Expr, + parent_options: ASTFlags, + op_flags: ASTFlags, op_pos: Position, ) -> ParseResult { match (lhs, rhs) { // lhs[idx_expr].rhs (Expr::Index(mut x, options, pos), rhs) => { - x.rhs = Self::make_dot_expr(state, x.rhs, options | parent_options, rhs, op_pos)?; + x.rhs = Self::make_dot_expr( + state, + x.rhs, + rhs, + options | parent_options, + op_flags, + op_pos, + )?; Ok(Expr::Index(x, ASTFlags::NONE, pos)) } // lhs.module::id - syntax error @@ -1977,16 +1990,12 @@ impl Engine { // lhs.id (lhs, var_expr @ Expr::Variable(..)) => { let rhs = var_expr.into_property(state); - Ok(Expr::Dot( - BinaryExpr { lhs, rhs }.into(), - ASTFlags::NONE, - op_pos, - )) + Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos)) } // lhs.prop (lhs, prop @ Expr::Property(..)) => Ok(Expr::Dot( BinaryExpr { lhs, rhs: prop }.into(), - ASTFlags::NONE, + op_flags, op_pos, )), // lhs.nnn::func(...) - syntax error @@ -2023,17 +2032,13 @@ impl Engine { ); let rhs = Expr::MethodCall(func, func_pos); - Ok(Expr::Dot( - BinaryExpr { lhs, rhs }.into(), - ASTFlags::NONE, - op_pos, - )) + Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos)) } // lhs.dot_lhs.dot_rhs or lhs.dot_lhs[idx_rhs] (lhs, rhs @ (Expr::Dot(..) | Expr::Index(..))) => { - let (x, term, pos, is_dot) = match rhs { - Expr::Dot(x, term, pos) => (x, term, pos, true), - Expr::Index(x, term, pos) => (x, term, pos, false), + let (x, options, pos, is_dot) = match rhs { + Expr::Dot(x, options, pos) => (x, options, pos, true), + Expr::Index(x, options, pos) => (x, options, pos, false), expr => unreachable!("Expr::Dot or Expr::Index expected but gets {:?}", expr), }; @@ -2050,22 +2055,18 @@ impl Engine { } // lhs.id.dot_rhs or lhs.id[idx_rhs] Expr::Variable(..) | Expr::Property(..) => { - let new_lhs = BinaryExpr { + let new_binary = BinaryExpr { lhs: x.lhs.into_property(state), rhs: x.rhs, } .into(); let rhs = if is_dot { - Expr::Dot(new_lhs, term, pos) + Expr::Dot(new_binary, options, pos) } else { - Expr::Index(new_lhs, term, pos) + Expr::Index(new_binary, options, pos) }; - Ok(Expr::Dot( - BinaryExpr { lhs, rhs }.into(), - ASTFlags::NONE, - op_pos, - )) + Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos)) } // lhs.func().dot_rhs or lhs.func()[idx_rhs] Expr::FnCall(mut func, func_pos) => { @@ -2083,15 +2084,11 @@ impl Engine { .into(); let rhs = if is_dot { - Expr::Dot(new_lhs, term, pos) + Expr::Dot(new_lhs, options, pos) } else { - Expr::Index(new_lhs, term, pos) + Expr::Index(new_lhs, options, pos) }; - Ok(Expr::Dot( - BinaryExpr { lhs, rhs }.into(), - ASTFlags::NONE, - op_pos, - )) + Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos)) } expr => unreachable!("invalid dot expression: {:?}", expr), } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 42e8a3b9..99aa7319 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -420,6 +420,8 @@ pub enum Token { Comma, /// `.` Period, + /// `?.` + Elvis, /// `..` ExclusiveRange, /// `..=` @@ -576,6 +578,7 @@ impl Token { Underscore => "_", Comma => ",", Period => ".", + Elvis => "?.", ExclusiveRange => "..", InclusiveRange => "..=", MapStart => "#{", @@ -771,6 +774,7 @@ impl Token { "_" => Underscore, "," => Comma, "." => Period, + "?." => Elvis, ".." => ExclusiveRange, "..=" => InclusiveRange, "#{" => MapStart, @@ -877,11 +881,12 @@ impl Token { use Token::*; match self { - LexError(..) | + LexError(..) | SemiColon | // ; - is unary Colon | // #{ foo: - is unary Comma | // ( ... , -expr ) - is unary //Period | + //Elvis | ExclusiveRange | // .. - is unary InclusiveRange | // ..= - is unary LeftBrace | // { -expr } - is unary @@ -987,12 +992,12 @@ impl Token { match self { LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus | UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift - | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | ExclusiveRange - | InclusiveRange | MapStart | Equals | LessThan | GreaterThan | LessThanEqualsTo - | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe | Or | XOr | Ampersand - | And | PlusAssign | MinusAssign | MultiplyAssign | DivideAssign | LeftShiftAssign - | RightShiftAssign | AndAssign | OrAssign | XOrAssign | ModuloAssign - | PowerOfAssign => true, + | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | Elvis + | ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan | GreaterThan + | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe + | Or | XOr | Ampersand | And | PlusAssign | MinusAssign | MultiplyAssign + | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign | OrAssign + | XOrAssign | ModuloAssign | PowerOfAssign => true, _ => false, } @@ -2033,7 +2038,10 @@ fn get_next_token_inner( ('$', ..) => return Some((Token::Reserved("$".into()), start_pos)), - ('?', '.') => return Some((Token::Reserved("?.".into()), start_pos)), + ('?', '.') => { + eat_next(stream, pos); + return Some((Token::Elvis, start_pos)); + } ('?', ..) => return Some((Token::Reserved("?".into()), start_pos)), (ch, ..) if ch.is_whitespace() => (), diff --git a/tests/get_set.rs b/tests/get_set.rs index c7efc3e1..2879d56b 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -390,3 +390,15 @@ fn test_get_set_indexer() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_get_set_elvis() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!(engine.eval::<()>("let x = (); x?.foo.bar.baz")?, ()); + assert_eq!(engine.eval::<()>("let x = (); x?.foo(1,2,3)")?, ()); + assert_eq!(engine.eval::<()>("let x = #{a:()}; x.a?.foo.bar.baz")?, ()); + assert_eq!(engine.eval::("let x = 'x'; x?.type_of()")?, "char"); + + Ok(()) +}