From 23bc476a9491764da496804e97e99413e95a9678 Mon Sep 17 00:00:00 2001 From: tofpie <75836434+tofpie@users.noreply.github.com> Date: Thu, 31 Dec 2020 20:42:39 +0100 Subject: [PATCH] Implement coalescing (?? and ??=) (#1013) * Implement coalescing (?? and ??=) * Add unit tests Co-authored-by: tofpie --- .../syntax/ast/node/operator/bin_op/mod.rs | 47 ++++-- boa/src/syntax/ast/node/operator/tests.rs | 19 +++ boa/src/syntax/ast/op.rs | 27 ++++ boa/src/syntax/ast/punctuator.rs | 8 + boa/src/syntax/lexer/mod.rs | 6 +- boa/src/syntax/lexer/operator.rs | 16 ++ boa/src/syntax/lexer/tests.rs | 4 +- .../expression/assignment/conditional.rs | 5 +- boa/src/syntax/parser/expression/mod.rs | 151 ++++++++++++------ boa/src/syntax/parser/expression/tests.rs | 49 +++++- 10 files changed, 259 insertions(+), 73 deletions(-) diff --git a/boa/src/syntax/ast/node/operator/bin_op/mod.rs b/boa/src/syntax/ast/node/operator/bin_op/mod.rs index 9bbe73b2e0..46419e3985 100644 --- a/boa/src/syntax/ast/node/operator/bin_op/mod.rs +++ b/boa/src/syntax/ast/node/operator/bin_op/mod.rs @@ -57,20 +57,27 @@ impl BinOp { } /// Runs the assignment operators. - fn run_assign(op: AssignOp, x: Value, y: Value, context: &mut Context) -> Result { + fn run_assign(op: AssignOp, x: Value, y: &Node, context: &mut Context) -> Result { match op { - AssignOp::Add => x.add(&y, context), - AssignOp::Sub => x.sub(&y, context), - AssignOp::Mul => x.mul(&y, context), - AssignOp::Exp => x.pow(&y, context), - AssignOp::Div => x.div(&y, context), - AssignOp::Mod => x.rem(&y, context), - AssignOp::And => x.bitand(&y, context), - AssignOp::Or => x.bitor(&y, context), - AssignOp::Xor => x.bitxor(&y, context), - AssignOp::Shl => x.shl(&y, context), - AssignOp::Shr => x.shr(&y, context), - AssignOp::Ushr => x.ushr(&y, context), + AssignOp::Add => x.add(&y.run(context)?, context), + AssignOp::Sub => x.sub(&y.run(context)?, context), + AssignOp::Mul => x.mul(&y.run(context)?, context), + AssignOp::Exp => x.pow(&y.run(context)?, context), + AssignOp::Div => x.div(&y.run(context)?, context), + AssignOp::Mod => x.rem(&y.run(context)?, context), + AssignOp::And => x.bitand(&y.run(context)?, context), + AssignOp::Or => x.bitor(&y.run(context)?, context), + AssignOp::Xor => x.bitxor(&y.run(context)?, context), + AssignOp::Shl => x.shl(&y.run(context)?, context), + AssignOp::Shr => x.shr(&y.run(context)?, context), + AssignOp::Ushr => x.ushr(&y.run(context)?, context), + AssignOp::Coalesce => { + if x.is_null_or_undefined() { + Ok(y.run(context)?) + } else { + Ok(x) + } + } } } } @@ -167,6 +174,14 @@ impl Executable for BinOp { self.rhs().run(context)? } } + LogOp::Coalesce => { + let left = self.lhs.run(context)?; + if left.is_null_or_undefined() { + self.rhs().run(context)? + } else { + left + } + } }), op::BinOp::Assign(op) => match self.lhs() { Node::Identifier(ref name) => { @@ -176,8 +191,7 @@ impl Executable for BinOp { .get_binding_value(name.as_ref()) .map_err(|e| e.to_error(context))?; - let v_b = self.rhs().run(context)?; - let value = Self::run_assign(op, v_a, v_b, context)?; + let value = Self::run_assign(op, v_a, self.rhs(), context)?; context .realm_mut() .environment @@ -188,8 +202,7 @@ impl Executable for BinOp { Node::GetConstField(ref get_const_field) => { let v_r_a = get_const_field.obj().run(context)?; let v_a = v_r_a.get_field(get_const_field.field()); - let v_b = self.rhs().run(context)?; - let value = Self::run_assign(op, v_a, v_b, context)?; + let value = Self::run_assign(op, v_a, self.rhs(), context)?; v_r_a.set_field(get_const_field.field(), value.clone()); Ok(value) } diff --git a/boa/src/syntax/ast/node/operator/tests.rs b/boa/src/syntax/ast/node/operator/tests.rs index bf5dea8c76..9196896878 100644 --- a/boa/src/syntax/ast/node/operator/tests.rs +++ b/boa/src/syntax/ast/node/operator/tests.rs @@ -60,3 +60,22 @@ fn instanceofoperator_rhs_not_callable() { "\"TypeError: right-hand side of 'instanceof' is not callable\"" ); } + +#[test] +fn logical_nullish_assignment() { + let scenario = r#" + let a = undefined; + a ??= 10; + a; + "#; + + assert_eq!(&exec(scenario), "10"); + + let scenario = r#" + let a = 20; + a ??= 10; + a; + "#; + + assert_eq!(&exec(scenario), "20"); +} diff --git a/boa/src/syntax/ast/op.rs b/boa/src/syntax/ast/op.rs index 4a876e2721..7f28da6a3a 100644 --- a/boa/src/syntax/ast/op.rs +++ b/boa/src/syntax/ast/op.rs @@ -683,6 +683,19 @@ pub enum LogOp { /// [spec]: https://tc39.es/ecma262/#prod-LogicalORExpression /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Logical_OR Or, + + /// The nullish coalescing operator is a logical operator that returns the second operand + /// when its first operand is null or undefined, and otherwise returns its first operand. + /// + /// Syntax: `x ?? y` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#prod-CoalesceExpression + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator + Coalesce, } impl Display for LogOp { @@ -693,6 +706,7 @@ impl Display for LogOp { match *self { Self::And => "&&", Self::Or => "||", + Self::Coalesce => "??", } ) } @@ -950,6 +964,18 @@ pub enum AssignOp { /// [spec]: https://tc39.es/ecma262/#prod-AssignmentOperator /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Unsigned_right_shift_assignment Ushr, + + /// The logical nullish assignment operator only assigns if the target variable is nullish (null or undefined). + /// + /// Syntax: `x ??= y` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#prod-AssignmentExpression + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_nullish_assignment + Coalesce, } unsafe impl Trace for AssignOp { @@ -974,6 +1000,7 @@ impl Display for AssignOp { Self::Shl => "<<=", Self::Shr => ">>=", Self::Ushr => ">>>=", + Self::Coalesce => "??=", } ) } diff --git a/boa/src/syntax/ast/punctuator.rs b/boa/src/syntax/ast/punctuator.rs index 1b40f92d6b..3b4c74a6e9 100644 --- a/boa/src/syntax/ast/punctuator.rs +++ b/boa/src/syntax/ast/punctuator.rs @@ -35,6 +35,8 @@ pub enum Punctuator { AssignAdd, /// `&=` AssignAnd, + /// `??=`, + AssignCoalesce, /// `/=` AssignDiv, /// `<<=` @@ -65,6 +67,8 @@ pub enum Punctuator { CloseBracket, /// `)` CloseParen, + /// `??` + Coalesce, /// `:` Colon, /// `,` @@ -137,6 +141,7 @@ impl Punctuator { match self { Self::AssignAdd => Some(BinOp::Assign(AssignOp::Add)), Self::AssignAnd => Some(BinOp::Assign(AssignOp::And)), + Self::AssignCoalesce => Some(BinOp::Assign(AssignOp::Coalesce)), Self::AssignDiv => Some(BinOp::Assign(AssignOp::Div)), Self::AssignLeftSh => Some(BinOp::Assign(AssignOp::Shl)), Self::AssignMod => Some(BinOp::Assign(AssignOp::Mod)), @@ -157,6 +162,7 @@ impl Punctuator { Self::Xor => Some(BinOp::Bit(BitOp::Xor)), Self::BoolAnd => Some(BinOp::Log(LogOp::And)), Self::BoolOr => Some(BinOp::Log(LogOp::Or)), + Self::Coalesce => Some(BinOp::Log(LogOp::Coalesce)), Self::Eq => Some(BinOp::Comp(CompOp::Equal)), Self::NotEq => Some(BinOp::Comp(CompOp::NotEqual)), Self::StrictEq => Some(BinOp::Comp(CompOp::StrictEqual)), @@ -194,6 +200,7 @@ impl Display for Punctuator { Self::Assign => "=", Self::AssignAdd => "+=", Self::AssignAnd => "&=", + Self::AssignCoalesce => "??=", Self::AssignDiv => "/=", Self::AssignLeftSh => "<<=", Self::AssignMod => "%=", @@ -206,6 +213,7 @@ impl Display for Punctuator { Self::AssignXor => "^=", Self::BoolAnd => "&&", Self::BoolOr => "||", + Self::Coalesce => "??", Self::CloseBlock => "}", Self::CloseBracket => "]", Self::CloseParen => ")", diff --git a/boa/src/syntax/lexer/mod.rs b/boa/src/syntax/lexer/mod.rs index 8728837675..d4ca507abb 100644 --- a/boa/src/syntax/lexer/mod.rs +++ b/boa/src/syntax/lexer/mod.rs @@ -248,12 +248,8 @@ impl Lexer { Punctuator::CloseBracket.into(), Span::new(start, self.cursor.pos()), )), - '?' => Ok(Token::new( - Punctuator::Question.into(), - Span::new(start, self.cursor.pos()), - )), '/' => self.lex_slash_token(start), - '=' | '*' | '+' | '-' | '%' | '|' | '&' | '^' | '<' | '>' | '!' | '~' => { + '=' | '*' | '+' | '-' | '%' | '|' | '&' | '^' | '<' | '>' | '!' | '~' | '?' => { Operator::new(next_ch as u8).lex(&mut self.cursor, start) } _ => { diff --git a/boa/src/syntax/lexer/operator.rs b/boa/src/syntax/lexer/operator.rs index 11971d384c..7de7750029 100644 --- a/boa/src/syntax/lexer/operator.rs +++ b/boa/src/syntax/lexer/operator.rs @@ -1,6 +1,7 @@ //! This module implements lexing for operators (+, - etc.) used in the JavaScript programing language. use super::{Cursor, Error, Tokenizer}; +use crate::syntax::lexer::TokenKind; use crate::{ profiler::BoaProfiler, syntax::{ @@ -123,6 +124,21 @@ impl Tokenizer for Operator { b'&' => op!(cursor, start_pos, Ok(Punctuator::AssignAnd), Ok(Punctuator::And), { Some(b'&') => Ok(Punctuator::BoolAnd) }), + b'?' => match cursor.peek()? { + Some(b'?') => { + let _ = cursor.next_byte()?.expect("? vanished"); + op!( + cursor, + start_pos, + Ok(Punctuator::AssignCoalesce), + Ok(Punctuator::Coalesce) + ) + } + _ => Ok(Token::new( + TokenKind::Punctuator(Punctuator::Question), + Span::new(start_pos, cursor.pos()), + )), + }, b'^' => op!( cursor, start_pos, diff --git a/boa/src/syntax/lexer/tests.rs b/boa/src/syntax/lexer/tests.rs index d8454afa7e..5806b81adc 100644 --- a/boa/src/syntax/lexer/tests.rs +++ b/boa/src/syntax/lexer/tests.rs @@ -109,7 +109,7 @@ fn check_punctuators() { // https://tc39.es/ecma262/#sec-punctuators let s = "{ ( ) [ ] . ... ; , < > <= >= == != === !== \ + - * % -- << >> >>> & | ^ ! ~ && || ? : \ - = += -= *= &= **= ++ ** <<= >>= >>>= &= |= ^= =>"; + = += -= *= &= **= ++ ** <<= >>= >>>= &= |= ^= => ?? ??="; let mut lexer = Lexer::new(s.as_bytes()); let expected = [ @@ -162,6 +162,8 @@ fn check_punctuators() { TokenKind::Punctuator(Punctuator::AssignOr), TokenKind::Punctuator(Punctuator::AssignXor), TokenKind::Punctuator(Punctuator::Arrow), + TokenKind::Punctuator(Punctuator::Coalesce), + TokenKind::Punctuator(Punctuator::AssignCoalesce), ]; expect_tokens(&mut lexer, &expected); diff --git a/boa/src/syntax/parser/expression/assignment/conditional.rs b/boa/src/syntax/parser/expression/assignment/conditional.rs index a45997ae5c..aa9998e415 100644 --- a/boa/src/syntax/parser/expression/assignment/conditional.rs +++ b/boa/src/syntax/parser/expression/assignment/conditional.rs @@ -12,7 +12,7 @@ use crate::{ syntax::{ ast::{node::ConditionalOp, Node, Punctuator}, parser::{ - expression::{AssignmentExpression, LogicalORExpression}, + expression::{AssignmentExpression, ShortCircuitExpression}, AllowAwait, AllowIn, AllowYield, Cursor, ParseResult, TokenParser, }, }, @@ -65,8 +65,7 @@ where fn parse(self, cursor: &mut Cursor) -> ParseResult { let _timer = BoaProfiler::global().start_event("ConditionalExpression", "Parsing"); - // TODO: coalesce expression - let lhs = LogicalORExpression::new(self.allow_in, self.allow_yield, self.allow_await) + let lhs = ShortCircuitExpression::new(self.allow_in, self.allow_yield, self.allow_await) .parse(cursor)?; if let Some(tok) = cursor.peek(0)? { diff --git a/boa/src/syntax/parser/expression/mod.rs b/boa/src/syntax/parser/expression/mod.rs index 68ff35d7d2..3d59270df0 100644 --- a/boa/src/syntax/parser/expression/mod.rs +++ b/boa/src/syntax/parser/expression/mod.rs @@ -20,15 +20,19 @@ pub(in crate::syntax::parser) mod await_expr; use self::assignment::ExponentiationExpression; pub(super) use self::{assignment::AssignmentExpression, primary::Initializer}; use super::{AllowAwait, AllowIn, AllowYield, Cursor, ParseResult, TokenParser}; -use crate::syntax::lexer::{InputElement, TokenKind}; + use crate::{ profiler::BoaProfiler, - syntax::ast::{ - node::{BinOp, Node}, - Keyword, Punctuator, + syntax::{ + ast::op::LogOp, + ast::{ + node::{BinOp, Node}, + Keyword, Punctuator, + }, + lexer::{InputElement, TokenKind}, + parser::ParseError, }, }; - use std::io::Read; // For use in the expression! macro to allow for both Punctuator and Keyword parameters. @@ -142,23 +146,31 @@ expression!( None:: ); -/// Parses a logical `OR` expression. +/// Parses a logical expression expression. /// /// More information: /// - [MDN documentation][mdn] /// - [ECMAScript specification][spec] /// -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Logical_OR_2 -/// [spec]: https://tc39.es/ecma262/#prod-LogicalORExpression +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators +/// [spec]: https://tc39.es/ecma262/#prod-ShortCircuitExpression #[derive(Debug, Clone, Copy)] -struct LogicalORExpression { +struct ShortCircuitExpression { allow_in: AllowIn, allow_yield: AllowYield, allow_await: AllowAwait, + previous: PreviousExpr, } -impl LogicalORExpression { - /// Creates a new `LogicalORExpression` parser. +#[derive(Debug, Clone, Copy, PartialEq)] +enum PreviousExpr { + None, + Logical, + Coalesce, +} + +impl ShortCircuitExpression { + /// Creates a new `ShortCircuitExpression` parser. pub(super) fn new(allow_in: I, allow_yield: Y, allow_await: A) -> Self where I: Into, @@ -169,36 +181,16 @@ impl LogicalORExpression { allow_in: allow_in.into(), allow_yield: allow_yield.into(), allow_await: allow_await.into(), + previous: PreviousExpr::None, } } -} - -expression!( - LogicalORExpression, - LogicalANDExpression, - [Punctuator::BoolOr], - [allow_in, allow_yield, allow_await], - None:: -); - -/// Parses a logical `AND` expression. -/// -/// More information: -/// - [MDN documentation][mdn] -/// - [ECMAScript specification][spec] -/// -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Logical_AND_2 -/// [spec]: https://tc39.es/ecma262/#prod-LogicalANDExpression -#[derive(Debug, Clone, Copy)] -struct LogicalANDExpression { - allow_in: AllowIn, - allow_yield: AllowYield, - allow_await: AllowAwait, -} -impl LogicalANDExpression { - /// Creates a new `LogicalANDExpression` parser. - pub(super) fn new(allow_in: I, allow_yield: Y, allow_await: A) -> Self + fn with_previous( + allow_in: I, + allow_yield: Y, + allow_await: A, + previous: PreviousExpr, + ) -> Self where I: Into, Y: Into, @@ -208,17 +200,86 @@ impl LogicalANDExpression { allow_in: allow_in.into(), allow_yield: allow_yield.into(), allow_await: allow_await.into(), + previous, } } } -expression!( - LogicalANDExpression, - BitwiseORExpression, - [Punctuator::BoolAnd], - [allow_in, allow_yield, allow_await], - None:: -); +impl TokenParser for ShortCircuitExpression +where + R: Read, +{ + type Output = Node; + + fn parse(self, cursor: &mut Cursor) -> ParseResult { + let _timer = BoaProfiler::global().start_event("ShortCircuitExpression", "Parsing"); + + let mut current_node = + BitwiseORExpression::new(self.allow_in, self.allow_yield, self.allow_await) + .parse(cursor)?; + let mut previous = self.previous; + + while let Some(tok) = cursor.peek(0)? { + match tok.kind() { + TokenKind::Punctuator(Punctuator::BoolAnd) => { + if previous == PreviousExpr::Coalesce { + return Err(ParseError::expected( + [TokenKind::Punctuator(Punctuator::Coalesce)], + tok.clone(), + "logical expression (cannot use '??' without parentheses within '||' or '&&')", + )); + } + let _ = cursor.next()?.expect("'&&' expected"); + previous = PreviousExpr::Logical; + let rhs = + BitwiseORExpression::new(self.allow_in, self.allow_yield, self.allow_await) + .parse(cursor)?; + + current_node = BinOp::new(LogOp::And, current_node, rhs).into(); + } + TokenKind::Punctuator(Punctuator::BoolOr) => { + if previous == PreviousExpr::Coalesce { + return Err(ParseError::expected( + [TokenKind::Punctuator(Punctuator::Coalesce)], + tok.clone(), + "logical expression (cannot use '??' without parentheses within '||' or '&&')", + )); + } + let _ = cursor.next()?.expect("'||' expected"); + previous = PreviousExpr::Logical; + let rhs = ShortCircuitExpression::with_previous( + self.allow_in, + self.allow_yield, + self.allow_await, + PreviousExpr::Logical, + ) + .parse(cursor)?; + current_node = BinOp::new(LogOp::Or, current_node, rhs).into(); + } + TokenKind::Punctuator(Punctuator::Coalesce) => { + if previous == PreviousExpr::Logical { + return Err(ParseError::expected( + [ + TokenKind::Punctuator(Punctuator::BoolAnd), + TokenKind::Punctuator(Punctuator::BoolOr), + ], + tok.clone(), + "cannot use '??' unparenthesized within '||' or '&&'", + )); + } + let _ = cursor.next()?.expect("'??' expected"); + previous = PreviousExpr::Coalesce; + let rhs = + BitwiseORExpression::new(self.allow_in, self.allow_yield, self.allow_await) + .parse(cursor)?; + current_node = BinOp::new(LogOp::Coalesce, current_node, rhs).into(); + } + _ => break, + } + } + Ok(current_node) + } +} /// Parses a bitwise `OR` expression. /// diff --git a/boa/src/syntax/parser/expression/tests.rs b/boa/src/syntax/parser/expression/tests.rs index 93642d43ba..1f1a882b1c 100644 --- a/boa/src/syntax/parser/expression/tests.rs +++ b/boa/src/syntax/parser/expression/tests.rs @@ -1,10 +1,10 @@ use crate::syntax::{ - ast::op::{AssignOp, BitOp, CompOp, NumOp}, + ast::op::{AssignOp, BitOp, CompOp, LogOp, NumOp}, ast::{ node::{BinOp, Identifier}, Const, }, - parser::tests::check_parser, + parser::tests::{check_invalid, check_parser}, }; /// Checks numeric operations @@ -191,6 +191,15 @@ fn check_assign_operations() { ) .into()], ); + check_parser( + "a ??= b", + vec![BinOp::new( + AssignOp::Coalesce, + Identifier::from("a"), + Identifier::from("b"), + ) + .into()], + ); } #[test] @@ -236,3 +245,39 @@ fn check_relational_operations() { vec![BinOp::new(CompOp::In, Identifier::from("p"), Identifier::from("o")).into()], ); } + +#[test] +fn check_logical_expressions() { + check_parser( + "a && b || c && d || e", + vec![BinOp::new( + LogOp::Or, + BinOp::new(LogOp::And, Identifier::from("a"), Identifier::from("b")), + BinOp::new( + LogOp::Or, + BinOp::new(LogOp::And, Identifier::from("c"), Identifier::from("d")), + Identifier::from("e"), + ), + ) + .into()], + ); + + check_parser( + "a ?? b ?? c", + vec![BinOp::new( + LogOp::Coalesce, + BinOp::new( + LogOp::Coalesce, + Identifier::from("a"), + Identifier::from("b"), + ), + Identifier::from("c"), + ) + .into()], + ); + + check_invalid("a ?? b && c"); + check_invalid("a && b ?? c"); + check_invalid("a ?? b || c"); + check_invalid("a || b ?? c"); +}