From 2b44778a5c268e0dc17e0abda54c3ac9cace0421 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 12 Jun 2022 00:32:12 +0800 Subject: [PATCH] Add elvis operator for indexing. --- CHANGELOG.md | 4 ++-- src/ast/expr.rs | 3 ++- src/eval/chaining.rs | 5 +++++ src/optimizer.rs | 6 ++++++ src/parser.rs | 24 +++++++++++++++------ src/tokenizer.rs | 51 ++++++++++++++++++++++++++++++++++++++------ tests/arrays.rs | 11 ++++++++++ 7 files changed, 88 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15545a79..447568f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 42cc91d2..a802e688 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -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, 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, _ => (), } diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index af62ebe5..810486a9 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -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 { diff --git a/src/optimizer.rs b/src/optimizer.rs index 1313495f..5fc60ee6 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -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) { diff --git a/src/parser.rs b/src/parser.rs index 4a1f1c16..d19c3d2d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -641,6 +641,7 @@ impl Engine { state: &mut ParseState, lib: &mut FnLib, lhs: Expr, + options: ASTFlags, check_index_type: bool, settings: ParseSettings, ) -> ParseResult { @@ -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"))] diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 1dd89768..bce10ea4 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -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() => (), diff --git a/tests/arrays.rs b/tests/arrays.rs index fb6e4a4d..aaea491a 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -405,3 +405,14 @@ fn test_arrays_map_reduce() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_arrays_elvis() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!(engine.eval::<()>("let x = (); x?[2]")?, ()); + + engine.run("let x = (); x?[2] = 42")?; + + Ok(()) +}