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 Reserved Symbols
---------------- ----------------
* `?`, `??`, `?.` and `!.` are now reserved symbols. * `?`, `??`, `?.`, `?[` and `!.` are now reserved symbols.
Deprecated API's Deprecated API's
---------------- ----------------
@ -29,7 +29,7 @@ Deprecated API's
New features 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. * The _null-coalescing operator_ (`??`) is now supported to short-circuit `()` values.
Enhancements Enhancements

View File

@ -411,6 +411,7 @@ pub enum Expr {
/// ///
/// ### Flags /// ### Flags
/// ///
/// [`NEGATED`][ASTFlags::NEGATED] = `?[` ... `]` (`[` ... `]` if unset)
/// [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset) /// [`BREAK`][ASTFlags::BREAK] = terminate the chain (recurse into the chain if unset)
Index(Box<BinaryExpr>, ASTFlags, Position), Index(Box<BinaryExpr>, ASTFlags, Position),
/// lhs `&&` rhs /// lhs `&&` rhs
@ -827,7 +828,7 @@ impl Expr {
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
Token::Period | Token::Elvis => return true, Token::Period | Token::Elvis => return true,
#[cfg(not(feature = "no_index"))] #[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 { match chain_type {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
ChainType::Indexing => { 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(); let pos = rhs.start_position();
match rhs { match rhs {

View File

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

View File

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

View File

@ -421,9 +421,17 @@ pub enum Token {
/// `.` /// `.`
Period, Period,
/// `?.` /// `?.`
///
/// Reserved under the `no_object` feature.
#[cfg(not(feature = "no_object"))]
Elvis, Elvis,
/// `??` /// `??`
DoubleQuestion, DoubleQuestion,
/// `?[`
///
/// Reserved under the `no_object` feature.
#[cfg(not(feature = "no_index"))]
QuestionBracket,
/// `..` /// `..`
ExclusiveRange, ExclusiveRange,
/// `..=` /// `..=`
@ -580,8 +588,11 @@ impl Token {
Underscore => "_", Underscore => "_",
Comma => ",", Comma => ",",
Period => ".", Period => ".",
#[cfg(not(feature = "no_object"))]
Elvis => "?.", Elvis => "?.",
DoubleQuestion => "??", DoubleQuestion => "??",
#[cfg(not(feature = "no_index"))]
QuestionBracket => "?[",
ExclusiveRange => "..", ExclusiveRange => "..",
InclusiveRange => "..=", InclusiveRange => "..=",
MapStart => "#{", MapStart => "#{",
@ -777,8 +788,11 @@ impl Token {
"_" => Underscore, "_" => Underscore,
"," => Comma, "," => Comma,
"." => Period, "." => Period,
#[cfg(not(feature = "no_object"))]
"?." => Elvis, "?." => Elvis,
"??" => DoubleQuestion, "??" => DoubleQuestion,
#[cfg(not(feature = "no_index"))]
"?[" => QuestionBracket,
".." => ExclusiveRange, ".." => ExclusiveRange,
"..=" => InclusiveRange, "..=" => InclusiveRange,
"#{" => MapStart, "#{" => MapStart,
@ -892,6 +906,7 @@ impl Token {
//Period | //Period |
//Elvis | //Elvis |
//DoubleQuestion | //DoubleQuestion |
//QuestionBracket |
ExclusiveRange | // .. - is unary ExclusiveRange | // .. - is unary
InclusiveRange | // ..= - is unary InclusiveRange | // ..= - is unary
LeftBrace | // { -expr } - is unary LeftBrace | // { -expr } - is unary
@ -999,12 +1014,18 @@ 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 | Elvis | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | DoubleQuestion
| DoubleQuestion | ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan | ExclusiveRange | InclusiveRange | MapStart | Equals | LessThan | GreaterThan
| GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo | Bang | Pipe
| Bang | Pipe | Or | XOr | Ampersand | And | PlusAssign | MinusAssign | Or | XOr | Ampersand | And | PlusAssign | MinusAssign | MultiplyAssign
| MultiplyAssign | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign | OrAssign
| OrAssign | XOrAssign | ModuloAssign | PowerOfAssign => true, | XOrAssign | ModuloAssign | PowerOfAssign => true,
#[cfg(not(feature = "no_object"))]
Elvis => true,
#[cfg(not(feature = "no_index"))]
QuestionBracket => true,
_ => false, _ => false,
} }
@ -2047,12 +2068,28 @@ fn get_next_token_inner(
('?', '.') => { ('?', '.') => {
eat_next(stream, pos); 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); eat_next(stream, pos);
return Some((Token::DoubleQuestion, start_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)), ('?', ..) => return Some((Token::Reserved("?".into()), start_pos)),
(ch, ..) if ch.is_whitespace() => (), (ch, ..) if ch.is_whitespace() => (),

View File

@ -405,3 +405,14 @@ fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
Ok(()) 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(())
}