Support Elvis operator.

This commit is contained in:
Stephen Chung 2022-06-10 10:26:06 +08:00
parent 206318e14c
commit 0f1e51b1c9
7 changed files with 104 additions and 60 deletions

View File

@ -26,6 +26,11 @@ Deprecated API's
* `FnPtr::num_curried` is deprecated in favor of `FnPtr::curry().len()`. * `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 Enhancements
------------ ------------

View File

@ -400,18 +400,18 @@ pub enum Expr {
Stmt(Box<StmtBlock>), Stmt(Box<StmtBlock>),
/// func `(` expr `,` ... `)` /// func `(` expr `,` ... `)`
FnCall(Box<FnCallExpr>, Position), FnCall(Box<FnCallExpr>, Position),
/// lhs `.` rhs /// lhs `.` rhs | lhs `?.` rhs
/// ///
/// ### Flags /// ### 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<BinaryExpr>, ASTFlags, Position), Dot(Box<BinaryExpr>, ASTFlags, Position),
/// lhs `[` rhs `]` /// lhs `[` rhs `]`
/// ///
/// ### Flags /// ### Flags
/// ///
/// [`NONE`][ASTFlags::NONE] = recurse into the indexing chain /// [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset)
/// [`BREAK`][ASTFlags::BREAK] = terminate the indexing chain
Index(Box<BinaryExpr>, ASTFlags, Position), Index(Box<BinaryExpr>, ASTFlags, Position),
/// lhs `&&` rhs /// lhs `&&` rhs
And(Box<BinaryExpr>, Position), And(Box<BinaryExpr>, Position),
@ -484,26 +484,37 @@ impl fmt::Debug for Expr {
f.debug_list().entries(x.iter()).finish() f.debug_list().entries(x.iter()).finish()
} }
Self::FnCall(x, ..) => fmt::Debug::fmt(x, f), Self::FnCall(x, ..) => fmt::Debug::fmt(x, f),
Self::Index(x, term, pos) => { Self::Index(x, options, pos) => {
if !pos.is_none() { if !pos.is_none() {
display_pos = format!(" @ {:?}", pos); display_pos = format!(" @ {:?}", pos);
} }
f.debug_struct("Index") let mut f = f.debug_struct("Index");
.field("lhs", &x.lhs)
.field("rhs", &x.rhs) f.field("lhs", &x.lhs).field("rhs", &x.rhs);
.field("terminate", term) if !options.is_empty() {
.finish() 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 { let op_name = match self {
Self::Dot(..) => "Dot",
Self::And(..) => "And", Self::And(..) => "And",
Self::Or(..) => "Or", Self::Or(..) => "Or",
expr => unreachable!( expr => unreachable!("Self::And or Self::Or expected but gets {:?}", expr),
"Self::Dot or Self::And or Self::Or expected but gets {:?}",
expr
),
}; };
if !pos.is_none() { if !pos.is_none() {
@ -802,7 +813,7 @@ impl Expr {
pub const fn is_valid_postfix(&self, token: &Token) -> bool { pub const fn is_valid_postfix(&self, token: &Token) -> bool {
match token { match token {
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
Token::Period => return true, Token::Period | Token::Elvis => return true,
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Token::LeftBracket => return true, Token::LeftBracket => return true,
_ => (), _ => (),

View File

@ -186,6 +186,11 @@ impl Engine {
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
ChainType::Dotting => { ChainType::Dotting => {
// Check for existence with the Elvis operator
if _parent_options.contains(ASTFlags::NEGATED) && target.is::<()>() {
return Ok((Dynamic::UNIT, false));
}
match rhs { match rhs {
// xxx.fn_name(arg_expr_list) // xxx.fn_name(arg_expr_list)
Expr::MethodCall(x, pos) if !x.is_qualified() && new_val.is_none() => { Expr::MethodCall(x, pos) if !x.is_qualified() && new_val.is_none() => {

View File

@ -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 // lhs.rhs
#[cfg(not(feature = "no_object"))] #[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 // map.string
(Expr::Map(m, pos), Expr::Property(p, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => { (Expr::Map(m, pos), Expr::Property(p, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => {
let prop = p.2.as_str(); let prop = p.2.as_str();
@ -932,7 +938,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
} }
// ....lhs.rhs // ....lhs.rhs
#[cfg(not(feature = "no_object"))] #[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] // lhs[rhs]
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]

View File

@ -1639,7 +1639,7 @@ impl Engine {
} }
// Property access // Property access
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
(expr, Token::Period) => { (expr, op @ Token::Period) | (expr, op @ Token::Elvis) => {
// Expression after dot must start with an identifier // Expression after dot must start with an identifier
match input.peek().expect(NEVER_ENDS) { match input.peek().expect(NEVER_ENDS) {
(Token::Identifier(..), ..) => { (Token::Identifier(..), ..) => {
@ -1654,7 +1654,12 @@ impl Engine {
} }
let rhs = self.parse_primary(input, state, lib, settings.level_up())?; 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 // Unknown postfix operator
(expr, token) => unreachable!( (expr, token) => unreachable!(
@ -1959,14 +1964,22 @@ impl Engine {
fn make_dot_expr( fn make_dot_expr(
state: &mut ParseState, state: &mut ParseState,
lhs: Expr, lhs: Expr,
parent_options: ASTFlags,
rhs: Expr, rhs: Expr,
parent_options: ASTFlags,
op_flags: ASTFlags,
op_pos: Position, op_pos: Position,
) -> ParseResult<Expr> { ) -> ParseResult<Expr> {
match (lhs, rhs) { match (lhs, rhs) {
// lhs[idx_expr].rhs // lhs[idx_expr].rhs
(Expr::Index(mut x, options, pos), 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)) Ok(Expr::Index(x, ASTFlags::NONE, pos))
} }
// lhs.module::id - syntax error // lhs.module::id - syntax error
@ -1977,16 +1990,12 @@ impl Engine {
// lhs.id // lhs.id
(lhs, var_expr @ Expr::Variable(..)) => { (lhs, var_expr @ Expr::Variable(..)) => {
let rhs = var_expr.into_property(state); let rhs = var_expr.into_property(state);
Ok(Expr::Dot( Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
BinaryExpr { lhs, rhs }.into(),
ASTFlags::NONE,
op_pos,
))
} }
// lhs.prop // lhs.prop
(lhs, prop @ Expr::Property(..)) => Ok(Expr::Dot( (lhs, prop @ Expr::Property(..)) => Ok(Expr::Dot(
BinaryExpr { lhs, rhs: prop }.into(), BinaryExpr { lhs, rhs: prop }.into(),
ASTFlags::NONE, op_flags,
op_pos, op_pos,
)), )),
// lhs.nnn::func(...) - syntax error // lhs.nnn::func(...) - syntax error
@ -2023,17 +2032,13 @@ impl Engine {
); );
let rhs = Expr::MethodCall(func, func_pos); let rhs = Expr::MethodCall(func, func_pos);
Ok(Expr::Dot( Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
BinaryExpr { lhs, rhs }.into(),
ASTFlags::NONE,
op_pos,
))
} }
// lhs.dot_lhs.dot_rhs or lhs.dot_lhs[idx_rhs] // lhs.dot_lhs.dot_rhs or lhs.dot_lhs[idx_rhs]
(lhs, rhs @ (Expr::Dot(..) | Expr::Index(..))) => { (lhs, rhs @ (Expr::Dot(..) | Expr::Index(..))) => {
let (x, term, pos, is_dot) = match rhs { let (x, options, pos, is_dot) = match rhs {
Expr::Dot(x, term, pos) => (x, term, pos, true), Expr::Dot(x, options, pos) => (x, options, pos, true),
Expr::Index(x, term, pos) => (x, term, pos, false), Expr::Index(x, options, pos) => (x, options, pos, false),
expr => unreachable!("Expr::Dot or Expr::Index expected but gets {:?}", expr), 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] // lhs.id.dot_rhs or lhs.id[idx_rhs]
Expr::Variable(..) | Expr::Property(..) => { Expr::Variable(..) | Expr::Property(..) => {
let new_lhs = BinaryExpr { let new_binary = BinaryExpr {
lhs: x.lhs.into_property(state), lhs: x.lhs.into_property(state),
rhs: x.rhs, rhs: x.rhs,
} }
.into(); .into();
let rhs = if is_dot { let rhs = if is_dot {
Expr::Dot(new_lhs, term, pos) Expr::Dot(new_binary, options, pos)
} else { } else {
Expr::Index(new_lhs, term, pos) Expr::Index(new_binary, options, pos)
}; };
Ok(Expr::Dot( Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
BinaryExpr { lhs, rhs }.into(),
ASTFlags::NONE,
op_pos,
))
} }
// lhs.func().dot_rhs or lhs.func()[idx_rhs] // lhs.func().dot_rhs or lhs.func()[idx_rhs]
Expr::FnCall(mut func, func_pos) => { Expr::FnCall(mut func, func_pos) => {
@ -2083,15 +2084,11 @@ impl Engine {
.into(); .into();
let rhs = if is_dot { let rhs = if is_dot {
Expr::Dot(new_lhs, term, pos) Expr::Dot(new_lhs, options, pos)
} else { } else {
Expr::Index(new_lhs, term, pos) Expr::Index(new_lhs, options, pos)
}; };
Ok(Expr::Dot( Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), op_flags, op_pos))
BinaryExpr { lhs, rhs }.into(),
ASTFlags::NONE,
op_pos,
))
} }
expr => unreachable!("invalid dot expression: {:?}", expr), expr => unreachable!("invalid dot expression: {:?}", expr),
} }

View File

@ -420,6 +420,8 @@ pub enum Token {
Comma, Comma,
/// `.` /// `.`
Period, Period,
/// `?.`
Elvis,
/// `..` /// `..`
ExclusiveRange, ExclusiveRange,
/// `..=` /// `..=`
@ -576,6 +578,7 @@ impl Token {
Underscore => "_", Underscore => "_",
Comma => ",", Comma => ",",
Period => ".", Period => ".",
Elvis => "?.",
ExclusiveRange => "..", ExclusiveRange => "..",
InclusiveRange => "..=", InclusiveRange => "..=",
MapStart => "#{", MapStart => "#{",
@ -771,6 +774,7 @@ impl Token {
"_" => Underscore, "_" => Underscore,
"," => Comma, "," => Comma,
"." => Period, "." => Period,
"?." => Elvis,
".." => ExclusiveRange, ".." => ExclusiveRange,
"..=" => InclusiveRange, "..=" => InclusiveRange,
"#{" => MapStart, "#{" => MapStart,
@ -877,11 +881,12 @@ impl Token {
use Token::*; use Token::*;
match self { match self {
LexError(..) | LexError(..) |
SemiColon | // ; - is unary SemiColon | // ; - is unary
Colon | // #{ foo: - is unary Colon | // #{ foo: - is unary
Comma | // ( ... , -expr ) - is unary Comma | // ( ... , -expr ) - is unary
//Period | //Period |
//Elvis |
ExclusiveRange | // .. - is unary ExclusiveRange | // .. - is unary
InclusiveRange | // ..= - is unary InclusiveRange | // ..= - is unary
LeftBrace | // { -expr } - is unary LeftBrace | // { -expr } - is unary
@ -987,12 +992,12 @@ impl Token {
match self { match self {
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 | ExclusiveRange | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | Elvis
| InclusiveRange | MapStart | Equals | LessThan | GreaterThan | LessThanEqualsTo | ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan | GreaterThan
| GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe | Or | XOr | Ampersand | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe
| And | PlusAssign | MinusAssign | MultiplyAssign | DivideAssign | LeftShiftAssign | Or | XOr | Ampersand | And | PlusAssign | MinusAssign | MultiplyAssign
| RightShiftAssign | AndAssign | OrAssign | XOrAssign | ModuloAssign | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign | OrAssign
| PowerOfAssign => true, | XOrAssign | ModuloAssign | PowerOfAssign => true,
_ => false, _ => false,
} }
@ -2033,7 +2038,10 @@ fn get_next_token_inner(
('$', ..) => return Some((Token::Reserved("$".into()), start_pos)), ('$', ..) => 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)), ('?', ..) => return Some((Token::Reserved("?".into()), start_pos)),
(ch, ..) if ch.is_whitespace() => (), (ch, ..) if ch.is_whitespace() => (),

View File

@ -390,3 +390,15 @@ fn test_get_set_indexer() -> Result<(), Box<EvalAltResult>> {
Ok(()) Ok(())
} }
#[test]
fn test_get_set_elvis() -> Result<(), Box<EvalAltResult>> {
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::<String>("let x = 'x'; x?.type_of()")?, "char");
Ok(())
}