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()`.
New features
------------
* The _Elvis operator_ (`?.`) is now supported for property access and method calls.
Enhancements
------------

View File

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

View File

@ -186,6 +186,11 @@ impl Engine {
#[cfg(not(feature = "no_object"))]
ChainType::Dotting => {
// Check for existence with the Elvis operator
if _parent_options.contains(ASTFlags::NEGATED) && target.is::<()>() {
return Ok((Dynamic::UNIT, false));
}
match rhs {
// xxx.fn_name(arg_expr_list)
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
#[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
(Expr::Map(m, pos), Expr::Property(p, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => {
let prop = p.2.as_str();
@ -932,7 +938,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
}
// ....lhs.rhs
#[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]
#[cfg(not(feature = "no_index"))]

View File

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

View File

@ -420,6 +420,8 @@ pub enum Token {
Comma,
/// `.`
Period,
/// `?.`
Elvis,
/// `..`
ExclusiveRange,
/// `..=`
@ -576,6 +578,7 @@ impl Token {
Underscore => "_",
Comma => ",",
Period => ".",
Elvis => "?.",
ExclusiveRange => "..",
InclusiveRange => "..=",
MapStart => "#{",
@ -771,6 +774,7 @@ impl Token {
"_" => Underscore,
"," => Comma,
"." => Period,
"?." => Elvis,
".." => ExclusiveRange,
"..=" => InclusiveRange,
"#{" => MapStart,
@ -877,11 +881,12 @@ impl Token {
use Token::*;
match self {
LexError(..) |
LexError(..) |
SemiColon | // ; - is unary
Colon | // #{ foo: - is unary
Comma | // ( ... , -expr ) - is unary
//Period |
//Elvis |
ExclusiveRange | // .. - is unary
InclusiveRange | // ..= - is unary
LeftBrace | // { -expr } - is unary
@ -987,12 +992,12 @@ impl Token {
match self {
LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus
| UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift
| RightShift | SemiColon | Colon | DoubleColon | Comma | Period | 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,
| 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,
_ => false,
}
@ -2033,7 +2038,10 @@ fn get_next_token_inner(
('$', ..) => 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)),
(ch, ..) if ch.is_whitespace() => (),

View File

@ -390,3 +390,15 @@ fn test_get_set_indexer() -> Result<(), Box<EvalAltResult>> {
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(())
}