Add null coalescing operator.
This commit is contained in:
parent
0f1e51b1c9
commit
8999872d62
@ -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
|
||||||
------------
|
------------
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 => {
|
||||||
|
@ -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);
|
||||||
|
@ -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() => (),
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user