Add elvis operator for indexing.

This commit is contained in:
Stephen Chung 2022-06-12 00:32:12 +08:00
parent b9cbeb65d6
commit 2b44778a5c
7 changed files with 88 additions and 16 deletions

View File

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

View File

@ -411,6 +411,7 @@ pub enum Expr {
///
/// ### Flags
///
/// [`NEGATED`][ASTFlags::NEGATED] = `?[` ... `]` (`[` ... `]` if unset)
/// [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset)
Index(Box<BinaryExpr>, ASTFlags, Position),
/// lhs `&&` rhs
@ -827,7 +828,7 @@ impl Expr {
#[cfg(not(feature = "no_object"))]
Token::Period | Token::Elvis => return true,
#[cfg(not(feature = "no_index"))]
Token::LeftBracket => return true,
Token::LeftBracket | Token::QuestionBracket => return true,
_ => (),
}

View File

@ -60,6 +60,11 @@ impl Engine {
match chain_type {
#[cfg(not(feature = "no_index"))]
ChainType::Indexing => {
// Check for existence with the null conditional operator
if _parent_options.contains(ASTFlags::NEGATED) && target.is::<()>() {
return Ok((Dynamic::UNIT, false));
}
let pos = rhs.start_position();
match rhs {

View File

@ -940,6 +940,12 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
#[cfg(not(feature = "no_object"))]
Expr::Dot(x,..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); }
// ()?[rhs]
#[cfg(not(feature = "no_index"))]
Expr::Index(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_index"))]
Expr::Index(x, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) {

View File

@ -641,6 +641,7 @@ impl Engine {
state: &mut ParseState,
lib: &mut FnLib,
lhs: Expr,
options: ASTFlags,
check_index_type: bool,
settings: ParseSettings,
) -> ParseResult<Expr> {
@ -756,29 +757,35 @@ impl Engine {
// Any more indexing following?
match input.peek().expect(NEVER_ENDS) {
// If another indexing level, right-bind it
(Token::LeftBracket, ..) => {
(Token::LeftBracket, ..) | (Token::QuestionBracket, ..) => {
let (token, pos) = input.next().expect(NEVER_ENDS);
let prev_pos = settings.pos;
settings.pos = eat_token(input, Token::LeftBracket);
settings.pos = pos;
// Recursively parse the indexing chain, right-binding each
let idx_expr = self.parse_index_chain(
input,
state,
lib,
idx_expr,
match token {
Token::LeftBracket => ASTFlags::NONE,
Token::QuestionBracket => ASTFlags::NEGATED,
_ => unreachable!(),
},
false,
settings.level_up(),
)?;
// Indexing binds to right
Ok(Expr::Index(
BinaryExpr { lhs, rhs: idx_expr }.into(),
ASTFlags::NONE,
options,
prev_pos,
))
}
// Otherwise terminate the indexing chain
_ => Ok(Expr::Index(
BinaryExpr { lhs, rhs: idx_expr }.into(),
ASTFlags::BREAK,
options | ASTFlags::BREAK,
settings.pos,
)),
}
@ -1634,8 +1641,13 @@ impl Engine {
}
// Indexing
#[cfg(not(feature = "no_index"))]
(expr, Token::LeftBracket) => {
self.parse_index_chain(input, state, lib, expr, true, settings.level_up())?
(expr, token @ Token::LeftBracket) | (expr, token @ Token::QuestionBracket) => {
let opt = match token {
Token::LeftBracket => ASTFlags::NONE,
Token::QuestionBracket => ASTFlags::NEGATED,
_ => unreachable!(),
};
self.parse_index_chain(input, state, lib, expr, opt, true, settings.level_up())?
}
// Property access
#[cfg(not(feature = "no_object"))]

View File

@ -421,9 +421,17 @@ pub enum Token {
/// `.`
Period,
/// `?.`
///
/// Reserved under the `no_object` feature.
#[cfg(not(feature = "no_object"))]
Elvis,
/// `??`
DoubleQuestion,
/// `?[`
///
/// Reserved under the `no_object` feature.
#[cfg(not(feature = "no_index"))]
QuestionBracket,
/// `..`
ExclusiveRange,
/// `..=`
@ -580,8 +588,11 @@ impl Token {
Underscore => "_",
Comma => ",",
Period => ".",
#[cfg(not(feature = "no_object"))]
Elvis => "?.",
DoubleQuestion => "??",
#[cfg(not(feature = "no_index"))]
QuestionBracket => "?[",
ExclusiveRange => "..",
InclusiveRange => "..=",
MapStart => "#{",
@ -777,8 +788,11 @@ impl Token {
"_" => Underscore,
"," => Comma,
"." => Period,
#[cfg(not(feature = "no_object"))]
"?." => Elvis,
"??" => DoubleQuestion,
#[cfg(not(feature = "no_index"))]
"?[" => QuestionBracket,
".." => ExclusiveRange,
"..=" => InclusiveRange,
"#{" => MapStart,
@ -892,6 +906,7 @@ impl Token {
//Period |
//Elvis |
//DoubleQuestion |
//QuestionBracket |
ExclusiveRange | // .. - is unary
InclusiveRange | // ..= - is unary
LeftBrace | // { -expr } - is unary
@ -999,12 +1014,18 @@ 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 | Elvis
| 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,
| RightShift | SemiColon | Colon | DoubleColon | Comma | Period | 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,
#[cfg(not(feature = "no_object"))]
Elvis => true,
#[cfg(not(feature = "no_index"))]
QuestionBracket => true,
_ => false,
}
@ -2047,12 +2068,28 @@ fn get_next_token_inner(
('?', '.') => {
eat_next(stream, pos);
return Some((Token::Elvis, start_pos));
return Some((
#[cfg(not(feature = "no_object"))]
Token::Elvis,
#[cfg(feature = "no_object")]
Token::Reserved("?.".into()),
start_pos,
));
}
('?', '?') => {
eat_next(stream, pos);
return Some((Token::DoubleQuestion, start_pos));
}
('?', '[') => {
eat_next(stream, pos);
return Some((
#[cfg(not(feature = "no_index"))]
Token::QuestionBracket,
#[cfg(feature = "no_index")]
Token::Reserved("?[".into()),
start_pos,
));
}
('?', ..) => return Some((Token::Reserved("?".into()), start_pos)),
(ch, ..) if ch.is_whitespace() => (),

View File

@ -405,3 +405,14 @@ fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_arrays_elvis() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(engine.eval::<()>("let x = (); x?[2]")?, ());
engine.run("let x = (); x?[2] = 42")?;
Ok(())
}