Browse Source

Implement coalescing (?? and ??=) (#1013)

* Implement coalescing (?? and ??=)

* Add unit tests

Co-authored-by: tofpie <tofpie@users.noreply.github.com>
pull/1015/head
tofpie 4 years ago committed by GitHub
parent
commit
23bc476a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 47
      boa/src/syntax/ast/node/operator/bin_op/mod.rs
  2. 19
      boa/src/syntax/ast/node/operator/tests.rs
  3. 27
      boa/src/syntax/ast/op.rs
  4. 8
      boa/src/syntax/ast/punctuator.rs
  5. 6
      boa/src/syntax/lexer/mod.rs
  6. 16
      boa/src/syntax/lexer/operator.rs
  7. 4
      boa/src/syntax/lexer/tests.rs
  8. 5
      boa/src/syntax/parser/expression/assignment/conditional.rs
  9. 151
      boa/src/syntax/parser/expression/mod.rs
  10. 49
      boa/src/syntax/parser/expression/tests.rs

47
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<Value> {
fn run_assign(op: AssignOp, x: Value, y: &Node, context: &mut Context) -> Result<Value> {
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)
}

19
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");
}

27
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 => "??=",
}
)
}

8
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 => ")",

6
boa/src/syntax/lexer/mod.rs

@ -248,12 +248,8 @@ impl<R> Lexer<R> {
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)
}
_ => {

16
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<R> Tokenizer<R> 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,

4
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);

5
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<R>) -> 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)? {

151
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::<InputElement>
);
/// 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<I, Y, A>(allow_in: I, allow_yield: Y, allow_await: A) -> Self
where
I: Into<AllowIn>,
@ -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::<InputElement>
);
/// 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<I, Y, A>(allow_in: I, allow_yield: Y, allow_await: A) -> Self
fn with_previous<I, Y, A>(
allow_in: I,
allow_yield: Y,
allow_await: A,
previous: PreviousExpr,
) -> Self
where
I: Into<AllowIn>,
Y: Into<AllowYield>,
@ -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::<InputElement>
);
impl<R> TokenParser<R> for ShortCircuitExpression
where
R: Read,
{
type Output = Node;
fn parse(self, cursor: &mut Cursor<R>) -> 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.
///

49
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");
}

Loading…
Cancel
Save