diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a47c966..15545a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Bug fixes Reserved Symbols ---------------- -* `?`, `?.` and `!.` are now reserved symbols. +* `?`, `??`, `?.` and `!.` are now reserved symbols. Deprecated API's ---------------- @@ -30,6 +30,7 @@ New features ------------ * The _Elvis operator_ (`?.`) is now supported for property access and method calls. +* The _null-coalescing operator_ (`??`) is now supported to short-circuit `()` values. Enhancements ------------ diff --git a/src/ast/expr.rs b/src/ast/expr.rs index e7fdd3fd..42cc91d2 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -417,6 +417,8 @@ pub enum Expr { And(Box, Position), /// lhs `||` rhs Or(Box, Position), + /// lhs `??` rhs + Coalesce(Box, Position), /// Custom syntax Custom(Box, Position), } @@ -510,11 +512,12 @@ impl fmt::Debug for Expr { } f.finish() } - Self::And(x, pos) | Self::Or(x, pos) => { + Self::And(x, pos) | Self::Or(x, pos) | Self::Coalesce(x, pos) => { let op_name = match self { Self::And(..) => "And", Self::Or(..) => "Or", - expr => unreachable!("Self::And or Self::Or expected but gets {:?}", expr), + Self::Coalesce(..) => "Coalesce", + expr => unreachable!("`And`, `Or` or `Coalesce` expected but gets {:?}", expr), }; if !pos.is_none() { @@ -696,6 +699,7 @@ impl Expr { | Self::Variable(.., pos) | Self::And(.., pos) | Self::Or(.., pos) + | Self::Coalesce(.., pos) | Self::Index(.., pos) | Self::Dot(.., pos) | Self::Custom(.., pos) @@ -721,10 +725,15 @@ impl Expr { self.position() } } - Self::And(x, ..) | Self::Or(x, ..) | Self::Index(x, ..) | Self::Dot(x, ..) => { - x.lhs.start_position() - } + + Self::And(x, ..) + | Self::Or(x, ..) + | Self::Coalesce(x, ..) + | Self::Index(x, ..) + | Self::Dot(x, ..) => x.lhs.start_position(), + Self::FnCall(.., pos) => *pos, + _ => self.position(), } } @@ -745,6 +754,7 @@ impl Expr { | Self::Map(.., pos) | Self::And(.., pos) | Self::Or(.., pos) + | Self::Coalesce(.., pos) | Self::Dot(.., pos) | Self::Index(.., pos) | Self::Variable(.., pos) @@ -770,7 +780,9 @@ impl Expr { Self::Map(x, ..) => x.0.iter().map(|(.., v)| v).all(Self::is_pure), - Self::And(x, ..) | Self::Or(x, ..) => x.lhs.is_pure() && x.rhs.is_pure(), + Self::And(x, ..) | Self::Or(x, ..) | Self::Coalesce(x, ..) => { + x.lhs.is_pure() && x.rhs.is_pure() + } Self::Stmt(x) => x.iter().all(Stmt::is_pure), @@ -828,6 +840,7 @@ impl Expr { | Self::CharConstant(..) | Self::And(..) | Self::Or(..) + | Self::Coalesce(..) | Self::Unit(..) => false, Self::IntegerConstant(..) @@ -892,7 +905,11 @@ impl Expr { } } } - Self::Index(x, ..) | Self::Dot(x, ..) | Expr::And(x, ..) | Expr::Or(x, ..) => { + Self::Index(x, ..) + | Self::Dot(x, ..) + | Expr::And(x, ..) + | Expr::Or(x, ..) + | Expr::Coalesce(x, ..) => { if !x.lhs.walk(path, on_node) { return false; } diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 7077637c..9e9aa7be 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -464,6 +464,17 @@ impl Engine { } } + Expr::Coalesce(x, ..) => { + let lhs = self.eval_expr(scope, global, caches, lib, this_ptr, &x.lhs, level); + + match lhs { + Ok(value) if value.is::<()>() => { + self.eval_expr(scope, global, caches, lib, this_ptr, &x.rhs, level) + } + Ok(_) | Err(_) => lhs, + } + } + Expr::Custom(custom, pos) => { let expressions: StaticVec<_> = custom.inputs.iter().map(Into::into).collect(); // The first token acts as the custom syntax's key diff --git a/src/optimizer.rs b/src/optimizer.rs index ac4a811e..1313495f 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -1073,6 +1073,16 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) { // lhs || rhs (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); } }, + // () ?? rhs -> rhs + Expr::Coalesce(x, ..) if matches!(x.lhs, Expr::Unit(..)) => { + state.set_dirty(); + *expr = mem::take(&mut x.rhs); + }, + // lhs:constant ?? rhs -> lhs + Expr::Coalesce(x, ..) if x.lhs.is_constant() => { + state.set_dirty(); + *expr = mem::take(&mut x.lhs); + }, // eval! Expr::FnCall(x, ..) if x.name == KEYWORD_EVAL => { diff --git a/src/parser.rs b/src/parser.rs index e7d63458..4a1f1c16 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1917,8 +1917,8 @@ impl Engine { } } } - // ??? && ??? = rhs, ??? || ??? = rhs - Expr::And(..) | Expr::Or(..) => Err(LexError::ImproperSymbol( + // ??? && ??? = rhs, ??? || ??? = rhs, xxx ?? xxx = rhs + Expr::And(..) | Expr::Or(..) | Expr::Coalesce(..) => Err(LexError::ImproperSymbol( "=".to_string(), "Possibly a typo of '=='?".to_string(), ) @@ -2223,6 +2223,18 @@ impl Engine { pos, ) } + Token::DoubleQuestion => { + let rhs = args.pop().unwrap(); + let current_lhs = args.pop().unwrap(); + Expr::Coalesce( + BinaryExpr { + lhs: current_lhs, + rhs, + } + .into(), + pos, + ) + } Token::In => { // Swap the arguments let current_lhs = args.remove(0); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 99aa7319..1dd89768 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -422,6 +422,8 @@ pub enum Token { Period, /// `?.` Elvis, + /// `??` + DoubleQuestion, /// `..` ExclusiveRange, /// `..=` @@ -579,6 +581,7 @@ impl Token { Comma => ",", Period => ".", Elvis => "?.", + DoubleQuestion => "??", ExclusiveRange => "..", InclusiveRange => "..=", MapStart => "#{", @@ -775,6 +778,7 @@ impl Token { "," => Comma, "." => Period, "?." => Elvis, + "??" => DoubleQuestion, ".." => ExclusiveRange, "..=" => InclusiveRange, "#{" => MapStart, @@ -887,6 +891,7 @@ impl Token { Comma | // ( ... , -expr ) - is unary //Period | //Elvis | + //DoubleQuestion | ExclusiveRange | // .. - is unary InclusiveRange | // ..= - is unary LeftBrace | // { -expr } - is unary @@ -957,6 +962,8 @@ impl Token { LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo => 130, + DoubleQuestion => 135, + ExclusiveRange | InclusiveRange => 140, Plus | Minus => 150, @@ -993,11 +1000,11 @@ impl Token { LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus | UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift | 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, + | DoubleQuestion | 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, } @@ -2042,6 +2049,10 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::Elvis, start_pos)); } + ('?', '?') => { + eat_next(stream, pos); + return Some((Token::DoubleQuestion, start_pos)); + } ('?', ..) => return Some((Token::Reserved("?".into()), start_pos)), (ch, ..) if ch.is_whitespace() => (), diff --git a/tests/binary_ops.rs b/tests/binary_ops.rs index ca5c79db..a470a155 100644 --- a/tests/binary_ops.rs +++ b/tests/binary_ops.rs @@ -135,3 +135,13 @@ fn test_binary_ops() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_binary_ops_null_coalesce() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!(engine.eval::("let x = 42; x ?? 123")?, 42); + assert_eq!(engine.eval::("let x = (); x ?? 123")?, 123); + + Ok(()) +}