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
----------------
* `?`, `?.` 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
------------

View File

@ -417,6 +417,8 @@ pub enum Expr {
And(Box<BinaryExpr>, Position),
/// lhs `||` rhs
Or(Box<BinaryExpr>, Position),
/// lhs `??` rhs
Coalesce(Box<BinaryExpr>, Position),
/// Custom syntax
Custom(Box<CustomExpr>, 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;
}

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) => {
let expressions: StaticVec<_> = custom.inputs.iter().map(Into::into).collect();
// 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) => { 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 => {

View File

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

View File

@ -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() => (),

View File

@ -135,3 +135,13 @@ fn test_binary_ops() -> Result<(), Box<EvalAltResult>> {
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(())
}