Add null coalescing operator.

This commit is contained in:
Stephen Chung 2022-06-10 11:22:33 +08:00
parent 0f1e51b1c9
commit 8999872d62
7 changed files with 87 additions and 15 deletions

View File

@ -19,7 +19,7 @@ Bug fixes
Reserved Symbols Reserved Symbols
---------------- ----------------
* `?`, `?.` and `!.` are now reserved symbols. * `?`, `??`, `?.` and `!.` are now reserved symbols.
Deprecated API's Deprecated API's
---------------- ----------------
@ -30,6 +30,7 @@ New features
------------ ------------
* The _Elvis operator_ (`?.`) is now supported for property access and method calls. * The _Elvis operator_ (`?.`) is now supported for property access and method calls.
* The _null-coalescing operator_ (`??`) is now supported to short-circuit `()` values.
Enhancements Enhancements
------------ ------------

View File

@ -417,6 +417,8 @@ pub enum Expr {
And(Box<BinaryExpr>, Position), And(Box<BinaryExpr>, Position),
/// lhs `||` rhs /// lhs `||` rhs
Or(Box<BinaryExpr>, Position), Or(Box<BinaryExpr>, Position),
/// lhs `??` rhs
Coalesce(Box<BinaryExpr>, Position),
/// Custom syntax /// Custom syntax
Custom(Box<CustomExpr>, Position), Custom(Box<CustomExpr>, Position),
} }
@ -510,11 +512,12 @@ impl fmt::Debug for Expr {
} }
f.finish() 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 { let op_name = match self {
Self::And(..) => "And", Self::And(..) => "And",
Self::Or(..) => "Or", 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() { if !pos.is_none() {
@ -696,6 +699,7 @@ impl Expr {
| Self::Variable(.., pos) | Self::Variable(.., pos)
| Self::And(.., pos) | Self::And(.., pos)
| Self::Or(.., pos) | Self::Or(.., pos)
| Self::Coalesce(.., pos)
| Self::Index(.., pos) | Self::Index(.., pos)
| Self::Dot(.., pos) | Self::Dot(.., pos)
| Self::Custom(.., pos) | Self::Custom(.., pos)
@ -721,10 +725,15 @@ impl Expr {
self.position() 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::FnCall(.., pos) => *pos,
_ => self.position(), _ => self.position(),
} }
} }
@ -745,6 +754,7 @@ impl Expr {
| Self::Map(.., pos) | Self::Map(.., pos)
| Self::And(.., pos) | Self::And(.., pos)
| Self::Or(.., pos) | Self::Or(.., pos)
| Self::Coalesce(.., pos)
| Self::Dot(.., pos) | Self::Dot(.., pos)
| Self::Index(.., pos) | Self::Index(.., pos)
| Self::Variable(.., pos) | Self::Variable(.., pos)
@ -770,7 +780,9 @@ impl Expr {
Self::Map(x, ..) => x.0.iter().map(|(.., v)| v).all(Self::is_pure), 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), Self::Stmt(x) => x.iter().all(Stmt::is_pure),
@ -828,6 +840,7 @@ impl Expr {
| Self::CharConstant(..) | Self::CharConstant(..)
| Self::And(..) | Self::And(..)
| Self::Or(..) | Self::Or(..)
| Self::Coalesce(..)
| Self::Unit(..) => false, | Self::Unit(..) => false,
Self::IntegerConstant(..) 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) { if !x.lhs.walk(path, on_node) {
return false; return false;
} }

View File

@ -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) => { Expr::Custom(custom, pos) => {
let expressions: StaticVec<_> = custom.inputs.iter().map(Into::into).collect(); let expressions: StaticVec<_> = custom.inputs.iter().map(Into::into).collect();
// The first token acts as the custom syntax's key // The first token acts as the custom syntax's key

View File

@ -1073,6 +1073,16 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
// lhs || rhs // lhs || rhs
(lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); } (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! // eval!
Expr::FnCall(x, ..) if x.name == KEYWORD_EVAL => { Expr::FnCall(x, ..) if x.name == KEYWORD_EVAL => {

View File

@ -1917,8 +1917,8 @@ impl Engine {
} }
} }
} }
// ??? && ??? = rhs, ??? || ??? = rhs // ??? && ??? = rhs, ??? || ??? = rhs, xxx ?? xxx = rhs
Expr::And(..) | Expr::Or(..) => Err(LexError::ImproperSymbol( Expr::And(..) | Expr::Or(..) | Expr::Coalesce(..) => Err(LexError::ImproperSymbol(
"=".to_string(), "=".to_string(),
"Possibly a typo of '=='?".to_string(), "Possibly a typo of '=='?".to_string(),
) )
@ -2223,6 +2223,18 @@ impl Engine {
pos, 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 => { Token::In => {
// Swap the arguments // Swap the arguments
let current_lhs = args.remove(0); let current_lhs = args.remove(0);

View File

@ -422,6 +422,8 @@ pub enum Token {
Period, Period,
/// `?.` /// `?.`
Elvis, Elvis,
/// `??`
DoubleQuestion,
/// `..` /// `..`
ExclusiveRange, ExclusiveRange,
/// `..=` /// `..=`
@ -579,6 +581,7 @@ impl Token {
Comma => ",", Comma => ",",
Period => ".", Period => ".",
Elvis => "?.", Elvis => "?.",
DoubleQuestion => "??",
ExclusiveRange => "..", ExclusiveRange => "..",
InclusiveRange => "..=", InclusiveRange => "..=",
MapStart => "#{", MapStart => "#{",
@ -775,6 +778,7 @@ impl Token {
"," => Comma, "," => Comma,
"." => Period, "." => Period,
"?." => Elvis, "?." => Elvis,
"??" => DoubleQuestion,
".." => ExclusiveRange, ".." => ExclusiveRange,
"..=" => InclusiveRange, "..=" => InclusiveRange,
"#{" => MapStart, "#{" => MapStart,
@ -887,6 +891,7 @@ impl Token {
Comma | // ( ... , -expr ) - is unary Comma | // ( ... , -expr ) - is unary
//Period | //Period |
//Elvis | //Elvis |
//DoubleQuestion |
ExclusiveRange | // .. - is unary ExclusiveRange | // .. - is unary
InclusiveRange | // ..= - is unary InclusiveRange | // ..= - is unary
LeftBrace | // { -expr } - is unary LeftBrace | // { -expr } - is unary
@ -957,6 +962,8 @@ impl Token {
LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo => 130, LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo => 130,
DoubleQuestion => 135,
ExclusiveRange | InclusiveRange => 140, ExclusiveRange | InclusiveRange => 140,
Plus | Minus => 150, Plus | Minus => 150,
@ -993,11 +1000,11 @@ impl Token {
LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus
| UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift | UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift
| RightShift | SemiColon | Colon | DoubleColon | Comma | Period | Elvis | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | Elvis
| ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan | GreaterThan | DoubleQuestion | ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan
| LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo
| Or | XOr | Ampersand | And | PlusAssign | MinusAssign | MultiplyAssign | Bang | Pipe | Or | XOr | Ampersand | And | PlusAssign | MinusAssign
| DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign | OrAssign | MultiplyAssign | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign
| XOrAssign | ModuloAssign | PowerOfAssign => true, | OrAssign | XOrAssign | ModuloAssign | PowerOfAssign => true,
_ => false, _ => false,
} }
@ -2042,6 +2049,10 @@ fn get_next_token_inner(
eat_next(stream, pos); eat_next(stream, pos);
return Some((Token::Elvis, start_pos)); return Some((Token::Elvis, start_pos));
} }
('?', '?') => {
eat_next(stream, pos);
return Some((Token::DoubleQuestion, start_pos));
}
('?', ..) => return Some((Token::Reserved("?".into()), start_pos)), ('?', ..) => return Some((Token::Reserved("?".into()), start_pos)),
(ch, ..) if ch.is_whitespace() => (), (ch, ..) if ch.is_whitespace() => (),

View File

@ -135,3 +135,13 @@ fn test_binary_ops() -> Result<(), Box<EvalAltResult>> {
Ok(()) Ok(())
} }
#[test]
fn test_binary_ops_null_coalesce() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(engine.eval::<INT>("let x = 42; x ?? 123")?, 42);
assert_eq!(engine.eval::<INT>("let x = (); x ?? 123")?, 123);
Ok(())
}