Browse Source

Implement optional chains (#2390)

This Pull Request implements optional chains.

Example:

```Javascript
const adventurer = {
    name: 'Alice',
    cat: {
        name: 'Dinah'
    }
};

console.log(adventurer.cat?.name); // Dinah
console.log(adventurer.dog?.name); // undefined
```

Since I needed to implement `Opcode::RotateLeft`, and #2378 had an implementation for `Opcode::RotateRight`, I took the opportunity to integrate both ops into this PR (big thanks to @HalidOdat for the original implementation!).

This PR almost has 100% conformance for the `optional-chaining` test suite. However, there's this one [test](85373b4ce1/test/language/expressions/optional-chaining/member-expression.js) that can't be solved until we properly set function names for function expressions in object and class definitions.
pull/2391/head
José Julián Espina 2 years ago
parent
commit
4b892a931d
  1. 257
      boa_engine/src/bytecompiler/mod.rs
  2. 10
      boa_engine/src/syntax/ast/expression/mod.rs
  3. 202
      boa_engine/src/syntax/ast/expression/optional.rs
  4. 3
      boa_engine/src/syntax/ast/punctuator.rs
  5. 53
      boa_engine/src/syntax/lexer/cursor.rs
  6. 2
      boa_engine/src/syntax/lexer/identifier.rs
  7. 41
      boa_engine/src/syntax/lexer/operator.rs
  8. 8
      boa_engine/src/syntax/lexer/regex.rs
  9. 3
      boa_engine/src/syntax/lexer/tests.rs
  10. 61
      boa_engine/src/syntax/parser/expression/left_hand_side/mod.rs
  11. 168
      boa_engine/src/syntax/parser/expression/left_hand_side/optional/mod.rs
  12. 93
      boa_engine/src/syntax/parser/expression/left_hand_side/optional/tests.rs
  13. 6
      boa_engine/src/vm/code_block.rs
  14. 3
      boa_engine/src/vm/mod.rs
  15. 22
      boa_engine/src/vm/opcode/jump/mod.rs
  16. 41
      boa_engine/src/vm/opcode/mod.rs
  17. 38
      boa_engine/src/vm/opcode/swap/mod.rs

257
boa_engine/src/bytecompiler/mod.rs

@ -12,7 +12,7 @@ use crate::{
binary::{ArithmeticOp, BinaryOp, BitwiseOp, LogicalOp, RelationalOp}, binary::{ArithmeticOp, BinaryOp, BitwiseOp, LogicalOp, RelationalOp},
unary::UnaryOp, unary::UnaryOp,
}, },
Call, Identifier, New, Call, Identifier, New, Optional, OptionalOperationKind,
}, },
function::{ function::{
ArrowFunction, AsyncFunction, AsyncGenerator, Class, ClassElement, FormalParameterList, ArrowFunction, AsyncFunction, AsyncGenerator, Class, ClassElement, FormalParameterList,
@ -430,6 +430,14 @@ impl<'b> ByteCompiler<'b> {
Label { index } Label { index }
} }
#[inline]
fn jump_if_null_or_undefined(&mut self) -> Label {
let index = self.next_opcode_location();
self.emit(Opcode::JumpIfNullOrUndefined, &[Self::DUMMY_ADDRESS]);
Label { index }
}
/// Emit an opcode with a dummy operand. /// Emit an opcode with a dummy operand.
/// Return the `Label` of the operand. /// Return the `Label` of the operand.
#[inline] #[inline]
@ -1109,6 +1117,7 @@ impl<'b> ByteCompiler<'b> {
} }
}, },
PropertyDefinition::MethodDefinition(name, kind) => match kind { PropertyDefinition::MethodDefinition(name, kind) => match kind {
// TODO: set function name for getter and setters
MethodDefinition::Get(expr) => match name { MethodDefinition::Get(expr) => match name {
PropertyName::Literal(name) => { PropertyName::Literal(name) => {
self.function(expr.into(), NodeKind::Expression, true)?; self.function(expr.into(), NodeKind::Expression, true)?;
@ -1123,6 +1132,7 @@ impl<'b> ByteCompiler<'b> {
self.emit_opcode(Opcode::SetPropertyGetterByValue); self.emit_opcode(Opcode::SetPropertyGetterByValue);
} }
}, },
// TODO: set function name for getter and setters
MethodDefinition::Set(expr) => match name { MethodDefinition::Set(expr) => match name {
PropertyName::Literal(name) => { PropertyName::Literal(name) => {
self.function(expr.into(), NodeKind::Expression, true)?; self.function(expr.into(), NodeKind::Expression, true)?;
@ -1443,12 +1453,201 @@ impl<'b> ByteCompiler<'b> {
self.emit_opcode(Opcode::PushNewTarget); self.emit_opcode(Opcode::PushNewTarget);
} }
} }
Expression::Optional(opt) => {
self.compile_optional_preserve_this(opt)?;
self.emit_opcode(Opcode::Swap);
self.emit_opcode(Opcode::Pop);
if !use_expr {
self.emit_opcode(Opcode::Pop);
}
}
// TODO: try to remove this variant somehow // TODO: try to remove this variant somehow
Expression::FormalParameterList(_) => unreachable!(), Expression::FormalParameterList(_) => unreachable!(),
} }
Ok(()) Ok(())
} }
/// Compile a property access expression, prepending `this` to the property value in the stack.
///
/// This compiles the access in a way that the state of the stack after executing the property
/// access becomes `...rest, this, value`. where `...rest` is the rest of the stack, `this` is the
/// `this` value of the access, and `value` is the final result of the access.
///
/// This is mostly useful for optional chains with calls (`a.b?.()`) and for regular chains
/// with calls (`a.b()`), since both of them must have `a` be the value of `this` for the function
/// call `b()`, but a regular compilation of the access would lose the `this` value after accessing
/// `b`.
fn compile_access_preserve_this(&mut self, access: &PropertyAccess) -> JsResult<()> {
match access {
PropertyAccess::Simple(access) => {
self.compile_expr(access.target(), true)?;
self.emit_opcode(Opcode::Dup);
match access.field() {
PropertyAccessField::Const(field) => {
let index = self.get_or_insert_name((*field).into());
self.emit(Opcode::GetPropertyByName, &[index]);
}
PropertyAccessField::Expr(field) => {
self.compile_expr(field, true)?;
self.emit_opcode(Opcode::Swap);
self.emit_opcode(Opcode::GetPropertyByValue);
}
}
}
PropertyAccess::Private(access) => {
self.compile_expr(access.target(), true)?;
self.emit_opcode(Opcode::Dup);
let index = self.get_or_insert_name(access.field().into());
self.emit(Opcode::GetPrivateField, &[index]);
}
PropertyAccess::Super(access) => {
self.emit_opcode(Opcode::This);
self.emit_opcode(Opcode::Super);
match access.field() {
PropertyAccessField::Const(field) => {
let index = self.get_or_insert_name((*field).into());
self.emit(Opcode::GetPropertyByName, &[index]);
}
PropertyAccessField::Expr(expr) => {
self.compile_expr(expr, true)?;
self.emit_opcode(Opcode::Swap);
self.emit_opcode(Opcode::GetPropertyByValue);
}
}
}
}
Ok(())
}
/// Compile an optional chain expression, prepending `this` to the property value in the stack.
///
/// This compiles the access in a way that the state of the stack after executing the optional
/// chain becomes `...rest, this, value`. where `...rest` is the rest of the stack, `this` is the
/// `this` value of the chain, and `value` is the result of the chain.
///
/// This is mostly useful for inner optional chains with external calls (`(a?.b)()`), because the
/// external call is not in the optional chain, and compiling an optional chain in the usual way
/// would only return the result of the chain without preserving the `this` value. In other words,
/// `this` would be set to `undefined` for that call, which is incorrect since `a` should be the
/// `this` value of the call.
fn compile_optional_preserve_this(&mut self, optional: &Optional) -> JsResult<()> {
let mut jumps = Vec::with_capacity(optional.chain().len());
match optional.target() {
Expression::PropertyAccess(access) => {
self.compile_access_preserve_this(access)?;
}
Expression::Optional(opt) => self.compile_optional_preserve_this(opt)?,
expr => {
self.emit(Opcode::PushUndefined, &[]);
self.compile_expr(expr, true)?;
}
}
jumps.push(self.jump_if_null_or_undefined());
let (first, rest) = optional
.chain()
.split_first()
.expect("chain must have at least one element");
assert!(first.shorted());
self.compile_optional_item_kind(first.kind())?;
for item in rest {
if item.shorted() {
jumps.push(self.jump_if_null_or_undefined());
}
self.compile_optional_item_kind(item.kind())?;
}
let skip_undef = self.jump();
for label in jumps {
self.patch_jump(label);
}
self.emit_opcode(Opcode::Pop);
self.emit_opcode(Opcode::PushUndefined);
self.patch_jump(skip_undef);
Ok(())
}
/// Compile a single operation in an optional chain.
///
/// On successful compilation, the state of the stack on execution will become `...rest, this, value`,
/// where `this` is the target of the property access (`undefined` on calls), and `value` is the
/// result of executing the action.
/// For example, in the expression `a?.b.c()`, after compiling and executing:
///
/// - `a?.b`, the state of the stack will become `...rest, a, b`.
/// - `b.c`, the state of the stack will become `...rest, b, c`.
/// - `c()`, the state of the stack will become `...rest, undefined, c()`.
///
/// # Requirements
/// - This should only be called after verifying that the previous value of the chain
/// is not null or undefined (if the operator `?.` was used).
/// - This assumes that the state of the stack before compiling is `...rest, this, value`,
/// since the operation compiled by this function could be a call.
fn compile_optional_item_kind(&mut self, kind: &OptionalOperationKind) -> JsResult<()> {
match kind {
OptionalOperationKind::SimplePropertyAccess { field } => {
self.emit_opcode(Opcode::Dup);
match field {
PropertyAccessField::Const(name) => {
let index = self.get_or_insert_name((*name).into());
self.emit(Opcode::GetPropertyByName, &[index]);
}
PropertyAccessField::Expr(expr) => {
self.compile_expr(expr, true)?;
self.emit(Opcode::Swap, &[]);
self.emit(Opcode::GetPropertyByValue, &[]);
}
}
self.emit_opcode(Opcode::RotateLeft);
self.emit_u8(3);
self.emit_opcode(Opcode::Pop);
}
OptionalOperationKind::PrivatePropertyAccess { field } => {
self.emit_opcode(Opcode::Dup);
let index = self.get_or_insert_name((*field).into());
self.emit(Opcode::GetPrivateField, &[index]);
self.emit_opcode(Opcode::RotateLeft);
self.emit_u8(3);
self.emit_opcode(Opcode::Pop);
}
OptionalOperationKind::Call { args } => {
let args = &**args;
let contains_spread = args.iter().any(|arg| matches!(arg, Expression::Spread(_)));
if contains_spread {
self.emit_opcode(Opcode::PushNewArray);
for arg in args {
self.compile_expr(arg, true)?;
if let Expression::Spread(_) = arg {
self.emit_opcode(Opcode::InitIterator);
self.emit_opcode(Opcode::PushIteratorToArray);
} else {
self.emit_opcode(Opcode::PushValueToArray);
}
}
self.emit_opcode(Opcode::CallSpread);
} else {
for arg in args {
self.compile_expr(arg, true)?;
}
self.emit(Opcode::Call, &[args.len() as u32]);
}
self.emit_opcode(Opcode::PushUndefined);
self.emit_opcode(Opcode::Swap);
}
}
Ok(())
}
pub fn compile_var_decl(&mut self, decl: &VarDeclaration) -> JsResult<()> { pub fn compile_var_decl(&mut self, decl: &VarDeclaration) -> JsResult<()> {
for variable in decl.0.as_ref() { for variable in decl.0.as_ref() {
match variable.binding() { match variable.binding() {
@ -2292,7 +2491,6 @@ impl<'b> ByteCompiler<'b> {
function.is_async(), function.is_async(),
function.is_arrow(), function.is_arrow(),
); );
let FunctionSpec { let FunctionSpec {
name, name,
parameters, parameters,
@ -2357,50 +2555,13 @@ impl<'b> ByteCompiler<'b> {
}; };
match call.function() { match call.function() {
Expression::PropertyAccess(access) => match access { Expression::PropertyAccess(access) if kind == CallKind::Call => {
PropertyAccess::Simple(access) => { self.compile_access_preserve_this(access)?;
self.compile_expr(access.target(), true)?; }
if kind == CallKind::Call {
self.emit(Opcode::Dup, &[]); Expression::Optional(opt) if kind == CallKind::Call => {
} self.compile_optional_preserve_this(opt)?;
match access.field() { }
PropertyAccessField::Const(field) => {
let index = self.get_or_insert_name((*field).into());
self.emit(Opcode::GetPropertyByName, &[index]);
}
PropertyAccessField::Expr(field) => {
self.compile_expr(field, true)?;
self.emit(Opcode::Swap, &[]);
self.emit(Opcode::GetPropertyByValue, &[]);
}
}
}
PropertyAccess::Private(access) => {
self.compile_expr(access.target(), true)?;
if kind == CallKind::Call {
self.emit(Opcode::Dup, &[]);
}
let index = self.get_or_insert_name(access.field().into());
self.emit(Opcode::GetPrivateField, &[index]);
}
PropertyAccess::Super(access) => {
if kind == CallKind::Call {
self.emit_opcode(Opcode::This);
}
self.emit_opcode(Opcode::Super);
match access.field() {
PropertyAccessField::Const(field) => {
let index = self.get_or_insert_name((*field).into());
self.emit(Opcode::GetPropertyByName, &[index]);
}
PropertyAccessField::Expr(expr) => {
self.compile_expr(expr, true)?;
self.emit_opcode(Opcode::Swap);
self.emit_opcode(Opcode::GetPropertyByValue);
}
}
}
},
expr => { expr => {
self.compile_expr(expr, true)?; self.compile_expr(expr, true)?;
if kind == CallKind::Call || kind == CallKind::CallEval { if kind == CallKind::Call || kind == CallKind::CallEval {
@ -2958,6 +3119,7 @@ impl<'b> ByteCompiler<'b> {
self.emit_opcode(Opcode::SetClassPrototype); self.emit_opcode(Opcode::SetClassPrototype);
self.emit_opcode(Opcode::Swap); self.emit_opcode(Opcode::Swap);
// TODO: set function name for getter and setters
for element in class.elements() { for element in class.elements() {
match element { match element {
ClassElement::StaticMethodDefinition(name, method_definition) => { ClassElement::StaticMethodDefinition(name, method_definition) => {
@ -3049,6 +3211,7 @@ impl<'b> ByteCompiler<'b> {
}, },
} }
} }
// TODO: set names for private methods
ClassElement::PrivateStaticMethodDefinition(name, method_definition) => { ClassElement::PrivateStaticMethodDefinition(name, method_definition) => {
self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::Dup);
match method_definition { match method_definition {
@ -3218,6 +3381,7 @@ impl<'b> ByteCompiler<'b> {
self.emit(Opcode::Call, &[0]); self.emit(Opcode::Call, &[0]);
self.emit_opcode(Opcode::Pop); self.emit_opcode(Opcode::Pop);
} }
// TODO: set names for private methods
ClassElement::PrivateMethodDefinition(name, method_definition) => { ClassElement::PrivateMethodDefinition(name, method_definition) => {
self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::Dup);
match method_definition { match method_definition {
@ -3263,6 +3427,7 @@ impl<'b> ByteCompiler<'b> {
match element { match element {
ClassElement::MethodDefinition(name, method_definition) => { ClassElement::MethodDefinition(name, method_definition) => {
self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::Dup);
// TODO: set names for getters and setters
match method_definition { match method_definition {
MethodDefinition::Get(expr) => match name { MethodDefinition::Get(expr) => match name {
PropertyName::Literal(name) => { PropertyName::Literal(name) => {

10
boa_engine/src/syntax/ast/expression/mod.rs

@ -27,6 +27,7 @@ mod r#await;
mod call; mod call;
mod identifier; mod identifier;
mod new; mod new;
mod optional;
mod spread; mod spread;
mod tagged_template; mod tagged_template;
mod r#yield; mod r#yield;
@ -34,6 +35,7 @@ mod r#yield;
pub use call::{Call, SuperCall}; pub use call::{Call, SuperCall};
pub use identifier::Identifier; pub use identifier::Identifier;
pub use new::New; pub use new::New;
pub use optional::{Optional, OptionalOperation, OptionalOperationKind};
pub use r#await::Await; pub use r#await::Await;
pub use r#yield::Yield; pub use r#yield::Yield;
pub use spread::Spread; pub use spread::Spread;
@ -110,10 +112,11 @@ pub enum Expression {
/// See [`Call`]. /// See [`Call`].
Call(Call), Call(Call),
/// See [`SuperCall`] /// See [`SuperCall`].
SuperCall(SuperCall), SuperCall(SuperCall),
// TODO: Optional chains /// See [`Optional`].
Optional(Optional),
// TODO: Import calls // TODO: Import calls
/// See [`TaggedTemplate`]. /// See [`TaggedTemplate`].
@ -173,6 +176,7 @@ impl Expression {
Self::New(new) => new.to_interned_string(interner), Self::New(new) => new.to_interned_string(interner),
Self::Call(call) => call.to_interned_string(interner), Self::Call(call) => call.to_interned_string(interner),
Self::SuperCall(supc) => supc.to_interned_string(interner), Self::SuperCall(supc) => supc.to_interned_string(interner),
Self::Optional(opt) => opt.to_interned_string(interner),
Self::NewTarget => "new.target".to_owned(), Self::NewTarget => "new.target".to_owned(),
Self::TaggedTemplate(tag) => tag.to_interned_string(interner), Self::TaggedTemplate(tag) => tag.to_interned_string(interner),
Self::Assign(assign) => assign.to_interned_string(interner), Self::Assign(assign) => assign.to_interned_string(interner),
@ -213,6 +217,7 @@ impl Expression {
Expression::New(new) => new.contains_arguments(), Expression::New(new) => new.contains_arguments(),
Expression::Call(call) => call.contains_arguments(), Expression::Call(call) => call.contains_arguments(),
Expression::SuperCall(call) => call.contains_arguments(), Expression::SuperCall(call) => call.contains_arguments(),
Expression::Optional(opt) => opt.contains_arguments(),
Expression::TaggedTemplate(tag) => tag.contains_arguments(), Expression::TaggedTemplate(tag) => tag.contains_arguments(),
Expression::Assign(assign) => assign.contains_arguments(), Expression::Assign(assign) => assign.contains_arguments(),
Expression::Unary(unary) => unary.contains_arguments(), Expression::Unary(unary) => unary.contains_arguments(),
@ -252,6 +257,7 @@ impl Expression {
Expression::Call(call) => call.contains(symbol), Expression::Call(call) => call.contains(symbol),
Expression::SuperCall(_) if symbol == ContainsSymbol::SuperCall => true, Expression::SuperCall(_) if symbol == ContainsSymbol::SuperCall => true,
Expression::SuperCall(expr) => expr.contains(symbol), Expression::SuperCall(expr) => expr.contains(symbol),
Expression::Optional(opt) => opt.contains(symbol),
Expression::TaggedTemplate(temp) => temp.contains(symbol), Expression::TaggedTemplate(temp) => temp.contains(symbol),
Expression::NewTarget => symbol == ContainsSymbol::NewTarget, Expression::NewTarget => symbol == ContainsSymbol::NewTarget,
Expression::Assign(assign) => assign.contains(symbol), Expression::Assign(assign) => assign.contains(symbol),

202
boa_engine/src/syntax/ast/expression/optional.rs

@ -0,0 +1,202 @@
use boa_interner::{Interner, Sym, ToInternedString};
use crate::syntax::ast::{join_nodes, ContainsSymbol};
use super::{access::PropertyAccessField, Expression};
/// List of valid operations in an [`Optional`] chain.
#[cfg_attr(feature = "deser", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub enum OptionalOperationKind {
/// A property access (`a?.prop`).
SimplePropertyAccess {
/// The field accessed.
field: PropertyAccessField,
},
/// A private property access (`a?.#prop`).
PrivatePropertyAccess {
/// The private property accessed.
field: Sym,
},
/// A function call (`a?.(arg)`).
Call {
/// The args passed to the function call.
args: Box<[Expression]>,
},
}
impl OptionalOperationKind {
#[inline]
pub(crate) fn contains_arguments(&self) -> bool {
match self {
OptionalOperationKind::SimplePropertyAccess { field } => field.contains_arguments(),
OptionalOperationKind::PrivatePropertyAccess { .. } => false,
OptionalOperationKind::Call { args } => args.iter().any(Expression::contains_arguments),
}
}
#[inline]
pub(crate) fn contains(&self, symbol: ContainsSymbol) -> bool {
match self {
OptionalOperationKind::SimplePropertyAccess { field } => field.contains(symbol),
OptionalOperationKind::PrivatePropertyAccess { .. } => false,
OptionalOperationKind::Call { args } => args.iter().any(|e| e.contains(symbol)),
}
}
}
/// Operation within an [`Optional`] chain.
///
/// An operation within an `Optional` chain can be either shorted or non-shorted. A shorted operation
/// (`?.item`) will force the expression to return `undefined` if the target is `undefined` or `null`.
/// In contrast, a non-shorted operation (`.prop`) will try to access the property, even if the target
/// is `undefined` or `null`.
#[cfg_attr(feature = "deser", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct OptionalOperation {
kind: OptionalOperationKind,
shorted: bool,
}
impl OptionalOperation {
/// Creates a new `OptionalOperation`.
#[inline]
pub fn new(kind: OptionalOperationKind, shorted: bool) -> Self {
Self { kind, shorted }
}
/// Gets the kind of operation.
#[inline]
pub fn kind(&self) -> &OptionalOperationKind {
&self.kind
}
/// Returns `true` if the operation short-circuits the [`Optional`] chain when the target is
/// `undefined` or `null`.
#[inline]
pub fn shorted(&self) -> bool {
self.shorted
}
#[inline]
pub(crate) fn contains_arguments(&self) -> bool {
self.kind.contains_arguments()
}
#[inline]
pub(crate) fn contains(&self, symbol: ContainsSymbol) -> bool {
self.kind.contains(symbol)
}
}
impl ToInternedString for OptionalOperation {
fn to_interned_string(&self, interner: &Interner) -> String {
let mut buf = if self.shorted {
String::from("?.")
} else {
if let OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(name),
} = &self.kind
{
return format!(".{}", interner.resolve_expect(*name));
}
if let OptionalOperationKind::PrivatePropertyAccess { field } = &self.kind {
return format!(".#{}", interner.resolve_expect(*field));
}
String::new()
};
buf.push_str(&match &self.kind {
OptionalOperationKind::SimplePropertyAccess { field } => match field {
PropertyAccessField::Const(name) => interner.resolve_expect(*name).to_string(),
PropertyAccessField::Expr(expr) => {
format!("[{}]", expr.to_interned_string(interner))
}
},
OptionalOperationKind::PrivatePropertyAccess { field } => {
format!("#{}", interner.resolve_expect(*field))
}
OptionalOperationKind::Call { args } => format!("({})", join_nodes(interner, args)),
});
buf
}
}
/// An optional chain expression, as defined by the [spec].
///
/// [Optional chaining][mdn] allows for short-circuiting property accesses and function calls, which
/// will return `undefined` instead of returning an error if the access target or the call is
/// either `undefined` or `null`.
///
/// An example of optional chaining:
///
/// ```Javascript
/// const adventurer = {
/// name: 'Alice',
/// cat: {
/// name: 'Dinah'
/// }
/// };
///
/// console.log(adventurer.cat?.name); // Dinah
/// console.log(adventurer.dog?.name); // undefined
/// ```
///
/// [spec]: https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-OptionalExpression
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
#[cfg_attr(feature = "deser", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Optional {
target: Box<Expression>,
chain: Box<[OptionalOperation]>,
}
impl Optional {
/// Creates a new `Optional` expression.
#[inline]
pub fn new(target: Expression, chain: Box<[OptionalOperation]>) -> Self {
Self {
target: Box::new(target),
chain,
}
}
/// Gets the target of this `Optional` expression.
#[inline]
pub fn target(&self) -> &Expression {
self.target.as_ref()
}
/// Gets the chain of accesses and calls that will be applied to the target at runtime.
#[inline]
pub fn chain(&self) -> &[OptionalOperation] {
self.chain.as_ref()
}
#[inline]
pub(crate) fn contains_arguments(&self) -> bool {
self.target.contains_arguments()
|| self.chain.iter().any(OptionalOperation::contains_arguments)
}
#[inline]
pub(crate) fn contains(&self, symbol: ContainsSymbol) -> bool {
self.target.contains(symbol) || self.chain.iter().any(|item| item.contains(symbol))
}
}
impl From<Optional> for Expression {
fn from(opt: Optional) -> Self {
Expression::Optional(opt)
}
}
impl ToInternedString for Optional {
fn to_interned_string(&self, interner: &Interner) -> String {
let mut buf = self.target.to_interned_string(interner);
for item in &*self.chain {
buf.push_str(&item.to_interned_string(interner));
}
buf
}
}

3
boa_engine/src/syntax/ast/punctuator.rs

@ -113,6 +113,8 @@ pub enum Punctuator {
OpenBracket, OpenBracket,
/// `(` /// `(`
OpenParen, OpenParen,
/// `?.`
Optional,
/// `|` /// `|`
Or, Or,
/// `**` /// `**`
@ -243,6 +245,7 @@ impl Punctuator {
Self::OpenBlock => "{", Self::OpenBlock => "{",
Self::OpenBracket => "[", Self::OpenBracket => "[",
Self::OpenParen => "(", Self::OpenParen => "(",
Self::Optional => "?.",
Self::Or => "|", Self::Or => "|",
Self::Exp => "**", Self::Exp => "**",
Self::Question => "?", Self::Question => "?",

53
boa_engine/src/syntax/lexer/cursor.rs

@ -79,7 +79,7 @@ where
/// Peeks the next n bytes, the maximum number of peeked bytes is 4 (n <= 4). /// Peeks the next n bytes, the maximum number of peeked bytes is 4 (n <= 4).
#[inline] #[inline]
pub(super) fn peek_n(&mut self, n: u8) -> Result<u32, Error> { pub(super) fn peek_n(&mut self, n: u8) -> Result<&[u8], Error> {
let _timer = Profiler::global().start_event("cursor::peek_n()", "Lexing"); let _timer = Profiler::global().start_event("cursor::peek_n()", "Lexing");
self.iter.peek_n_bytes(n) self.iter.peek_n_bytes(n)
@ -250,7 +250,7 @@ where
Some(0xE2) => { Some(0xE2) => {
// Try to match '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9) // Try to match '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9)
let next_bytes = self.peek_n(2)?; let next_bytes = self.peek_n(2)?;
if next_bytes == 0xA8_80 || next_bytes == 0xA9_80 { if next_bytes == [0x80, 0xA8] || next_bytes == [0x80, 0xA9] {
self.next_line(); self.next_line();
} else { } else {
// 0xE2 is a utf8 first byte // 0xE2 is a utf8 first byte
@ -296,7 +296,7 @@ where
struct InnerIter<R> { struct InnerIter<R> {
iter: Bytes<R>, iter: Bytes<R>,
num_peeked_bytes: u8, num_peeked_bytes: u8,
peeked_bytes: u32, peeked_bytes: [u8; 4],
peeked_char: Option<Option<u32>>, peeked_char: Option<Option<u32>>,
} }
@ -307,7 +307,7 @@ impl<R> InnerIter<R> {
Self { Self {
iter, iter,
num_peeked_bytes: 0, num_peeked_bytes: 0,
peeked_bytes: 0, peeked_bytes: [0; 4],
peeked_char: None, peeked_char: None,
} }
} }
@ -349,13 +349,13 @@ where
#[inline] #[inline]
pub(super) fn peek_byte(&mut self) -> Result<Option<u8>, Error> { pub(super) fn peek_byte(&mut self) -> Result<Option<u8>, Error> {
if self.num_peeked_bytes > 0 { if self.num_peeked_bytes > 0 {
let byte = self.peeked_bytes as u8; let byte = self.peeked_bytes[0];
Ok(Some(byte)) Ok(Some(byte))
} else { } else {
match self.iter.next().transpose()? { match self.iter.next().transpose()? {
Some(byte) => { Some(byte) => {
self.num_peeked_bytes = 1; self.num_peeked_bytes = 1;
self.peeked_bytes = u32::from(byte); self.peeked_bytes[0] = byte;
Ok(Some(byte)) Ok(Some(byte))
} }
None => Ok(None), None => Ok(None),
@ -365,24 +365,17 @@ where
/// Peeks the next n bytes, the maximum number of peeked bytes is 4 (n <= 4). /// Peeks the next n bytes, the maximum number of peeked bytes is 4 (n <= 4).
#[inline] #[inline]
pub(super) fn peek_n_bytes(&mut self, n: u8) -> Result<u32, Error> { pub(super) fn peek_n_bytes(&mut self, n: u8) -> Result<&[u8], Error> {
while self.num_peeked_bytes < n && self.num_peeked_bytes < 4 { while self.num_peeked_bytes < n && self.num_peeked_bytes < 4 {
match self.iter.next().transpose()? { match self.iter.next().transpose()? {
Some(byte) => { Some(byte) => {
self.peeked_bytes |= u32::from(byte) << (self.num_peeked_bytes * 8); self.peeked_bytes[usize::from(self.num_peeked_bytes)] = byte;
self.num_peeked_bytes += 1; self.num_peeked_bytes += 1;
} }
None => break, None => break,
}; };
} }
Ok(&self.peeked_bytes[..usize::from(u8::min(n, self.num_peeked_bytes))])
match n {
0 => Ok(0),
1 => Ok(self.peeked_bytes & 0xFF),
2 => Ok(self.peeked_bytes & 0xFFFF),
3 => Ok(self.peeked_bytes & 0xFFFFFF),
_ => Ok(self.peeked_bytes),
}
} }
/// Peeks the next unchecked character in u32 code point. /// Peeks the next unchecked character in u32 code point.
@ -392,34 +385,40 @@ where
Ok(ch) Ok(ch)
} else { } else {
// Decode UTF-8 // Decode UTF-8
let x = match self.peek_byte()? { let (x, y, z, w) = match self.peek_n_bytes(4)? {
Some(b) if b < 128 => { [b, ..] if *b < 128 => {
self.peeked_char = Some(Some(u32::from(b))); let char = u32::from(*b);
return Ok(Some(u32::from(b))); self.peeked_char = Some(Some(char));
return Ok(Some(char));
} }
Some(b) => b, [] => {
None => {
self.peeked_char = None; self.peeked_char = None;
return Ok(None); return Ok(None);
} }
bytes => (
bytes[0],
bytes.get(1).copied(),
bytes.get(2).copied(),
bytes.get(3).copied(),
),
}; };
// Multibyte case follows // Multibyte case follows
// Decode from a byte combination out of: [[[x y] z] w] // Decode from a byte combination out of: [[[x y] z] w]
// NOTE: Performance is sensitive to the exact formulation here // NOTE: Performance is sensitive to the exact formulation here
let init = utf8_first_byte(x, 2); let init = utf8_first_byte(x, 2);
let y = (self.peek_n_bytes(2)? >> 8) as u8; let y = y.unwrap_or_default();
let mut ch = utf8_acc_cont_byte(init, y); let mut ch = utf8_acc_cont_byte(init, y);
if x >= 0xE0 { if x >= 0xE0 {
// [[x y z] w] case // [[x y z] w] case
// 5th bit in 0xE0 .. 0xEF is always clear, so `init` is still valid // 5th bit in 0xE0 .. 0xEF is always clear, so `init` is still valid
let z = (self.peek_n_bytes(3)? >> 16) as u8; let z = z.unwrap_or_default();
let y_z = utf8_acc_cont_byte(u32::from(y & CONT_MASK), z); let y_z = utf8_acc_cont_byte(u32::from(y & CONT_MASK), z);
ch = init << 12 | y_z; ch = init << 12 | y_z;
if x >= 0xF0 { if x >= 0xF0 {
// [x y z w] case // [x y z w] case
// use only the lower 3 bits of `init` // use only the lower 3 bits of `init`
let w = (self.peek_n_bytes(4)? >> 24) as u8; let w = w.unwrap_or_default();
ch = (init & 7) << 18 | utf8_acc_cont_byte(y_z, w); ch = (init & 7) << 18 | utf8_acc_cont_byte(y_z, w);
} }
}; };
@ -434,9 +433,9 @@ where
fn next_byte(&mut self) -> io::Result<Option<u8>> { fn next_byte(&mut self) -> io::Result<Option<u8>> {
self.peeked_char = None; self.peeked_char = None;
if self.num_peeked_bytes > 0 { if self.num_peeked_bytes > 0 {
let byte = (self.peeked_bytes & 0xFF) as u8; let byte = self.peeked_bytes[0];
self.num_peeked_bytes -= 1; self.num_peeked_bytes -= 1;
self.peeked_bytes >>= 8; self.peeked_bytes.rotate_left(1);
Ok(Some(byte)) Ok(Some(byte))
} else { } else {
self.iter.next().transpose() self.iter.next().transpose()

2
boa_engine/src/syntax/lexer/identifier.rs

@ -124,7 +124,7 @@ impl Identifier {
loop { loop {
let ch = match cursor.peek_char()? { let ch = match cursor.peek_char()? {
Some(0x005C /* \ */) if cursor.peek_n(2)? >> 8 == 0x0075 /* u */ => { Some(0x005C /* \ */) if cursor.peek_n(2)?.get(1) == Some(&0x75) /* u */ => {
let pos = cursor.pos(); let pos = cursor.pos();
let _next = cursor.next_byte(); let _next = cursor.next_byte();
let _next = cursor.next_byte(); let _next = cursor.next_byte();

41
boa_engine/src/syntax/lexer/operator.rs

@ -126,21 +126,34 @@ impl<R> Tokenizer<R> for Operator {
b'&' => op!(cursor, start_pos, Ok(Punctuator::AssignAnd), Ok(Punctuator::And), { b'&' => op!(cursor, start_pos, Ok(Punctuator::AssignAnd), Ok(Punctuator::And), {
Some(b'&') => vop!(cursor, Ok(Punctuator::AssignBoolAnd), Ok(Punctuator::BoolAnd)) Some(b'&') => vop!(cursor, Ok(Punctuator::AssignBoolAnd), Ok(Punctuator::BoolAnd))
}), }),
b'?' => match cursor.peek()? { b'?' => {
Some(b'?') => { let (first, second) = (
let _ = cursor.next_byte()?.expect("? vanished"); cursor.peek_n(2)?.first().copied(),
op!( cursor.peek_n(2)?.get(1).copied(),
cursor, );
start_pos, match first {
Ok(Punctuator::AssignCoalesce), Some(b'?') => {
Ok(Punctuator::Coalesce) let _ = cursor.next_byte()?.expect("? vanished");
) op!(
cursor,
start_pos,
Ok(Punctuator::AssignCoalesce),
Ok(Punctuator::Coalesce)
)
}
Some(b'.') if !matches!(second, Some(second) if second.is_ascii_digit()) => {
let _ = cursor.next_byte()?.expect(". vanished");
Ok(Token::new(
TokenKind::Punctuator(Punctuator::Optional),
Span::new(start_pos, cursor.pos()),
))
}
_ => Ok(Token::new(
TokenKind::Punctuator(Punctuator::Question),
Span::new(start_pos, cursor.pos()),
)),
} }
_ => Ok(Token::new( }
TokenKind::Punctuator(Punctuator::Question),
Span::new(start_pos, cursor.pos()),
)),
},
b'^' => op!( b'^' => op!(
cursor, cursor,
start_pos, start_pos,

8
boa_engine/src/syntax/lexer/regex.rs

@ -71,7 +71,9 @@ impl<R> Tokenizer<R> for RegexLiteral {
cursor.pos(), cursor.pos(),
)); ));
} }
0xE2 if (cursor.peek_n(2)? == 0xA8_80 || cursor.peek_n(2)? == 0xA9_80) => { 0xE2 if (cursor.peek_n(2)? == [0x80, 0xA8]
|| cursor.peek_n(2)? == [0x80, 0xA9]) =>
{
// '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9) are not allowed // '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9) are not allowed
return Err(Error::syntax( return Err(Error::syntax(
"new lines are not allowed in regular expressions", "new lines are not allowed in regular expressions",
@ -90,8 +92,8 @@ impl<R> Tokenizer<R> for RegexLiteral {
cursor.pos(), cursor.pos(),
)); ));
} }
0xE2 if (cursor.peek_n(2)? == 0xA8_80 0xE2 if (cursor.peek_n(2)? == [0x80, 0xA8]
|| cursor.peek_n(2)? == 0xA9_80) => || cursor.peek_n(2)? == [0x80, 0xA9]) =>
{ {
// '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9) are not allowed // '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9) are not allowed
return Err(Error::syntax( return Err(Error::syntax(

3
boa_engine/src/syntax/lexer/tests.rs

@ -182,7 +182,7 @@ fn check_punctuators() {
// https://tc39.es/ecma262/#sec-punctuators // https://tc39.es/ecma262/#sec-punctuators
let s = "{ ( ) [ ] . ... ; , < > <= >= == != === !== \ let s = "{ ( ) [ ] . ... ; , < > <= >= == != === !== \
+ - * % -- << >> >>> & | ^ ! ~ && || ? : \ + - * % -- << >> >>> & | ^ ! ~ && || ? : \
= += -= *= &= **= ++ ** <<= >>= >>>= &= |= ^= => ?? ??= &&= ||="; = += -= *= &= **= ++ ** <<= >>= >>>= &= |= ^= => ?? ??= &&= ||= ?.";
let mut lexer = Lexer::new(s.as_bytes()); let mut lexer = Lexer::new(s.as_bytes());
let mut interner = Interner::default(); let mut interner = Interner::default();
@ -240,6 +240,7 @@ fn check_punctuators() {
TokenKind::Punctuator(Punctuator::AssignCoalesce), TokenKind::Punctuator(Punctuator::AssignCoalesce),
TokenKind::Punctuator(Punctuator::AssignBoolAnd), TokenKind::Punctuator(Punctuator::AssignBoolAnd),
TokenKind::Punctuator(Punctuator::AssignBoolOr), TokenKind::Punctuator(Punctuator::AssignBoolOr),
TokenKind::Punctuator(Punctuator::Optional),
]; ];
expect_tokens(&mut lexer, &expected, &mut interner); expect_tokens(&mut lexer, &expected, &mut interner);

61
boa_engine/src/syntax/parser/expression/left_hand_side/mod.rs

@ -13,6 +13,7 @@ mod tests;
mod arguments; mod arguments;
mod call; mod call;
mod member; mod member;
mod optional;
mod template; mod template;
use crate::syntax::{ use crate::syntax::{
@ -24,6 +25,7 @@ use crate::syntax::{
parser::{ parser::{
expression::left_hand_side::{ expression::left_hand_side::{
arguments::Arguments, call::CallExpression, member::MemberExpression, arguments::Arguments, call::CallExpression, member::MemberExpression,
optional::OptionalExpression,
}, },
AllowAwait, AllowYield, Cursor, ParseResult, TokenParser, AllowAwait, AllowYield, Cursor, ParseResult, TokenParser,
}, },
@ -70,32 +72,59 @@ where
type Output = Expression; type Output = Expression;
fn parse(self, cursor: &mut Cursor<R>, interner: &mut Interner) -> ParseResult<Self::Output> { fn parse(self, cursor: &mut Cursor<R>, interner: &mut Interner) -> ParseResult<Self::Output> {
/// Checks if we need to parse a super call expression `super()`.
///
/// It first checks if the next token is `super`, and if it is, it checks if the second next
/// token is the open parenthesis (`(`) punctuator.
///
/// This is needed because the `if let` chain is very complex, and putting it inline in the
/// initialization of `lhs` would make it very hard to return an expression over all
/// possible branches of the `if let`s. Instead, we extract the check into its own function,
/// then use it inside the condition of a simple `if ... else` expression.
fn is_super_call<R: Read>(
cursor: &mut Cursor<R>,
interner: &mut Interner,
) -> ParseResult<bool> {
if let Some(next) = cursor.peek(0, interner)? {
if let TokenKind::Keyword((Keyword::Super, _)) = next.kind() {
if let Some(next) = cursor.peek(1, interner)? {
if next.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) {
return Ok(true);
}
}
}
}
Ok(false)
}
let _timer = Profiler::global().start_event("LeftHandSideExpression", "Parsing"); let _timer = Profiler::global().start_event("LeftHandSideExpression", "Parsing");
cursor.set_goal(InputElement::TemplateTail); cursor.set_goal(InputElement::TemplateTail);
if let Some(next) = cursor.peek(0, interner)? { let mut lhs = if is_super_call(cursor, interner)? {
if let TokenKind::Keyword((Keyword::Super, _)) = next.kind() { cursor.next(interner).expect("token disappeared");
if let Some(next) = cursor.peek(1, interner)? { let args =
if next.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { Arguments::new(self.allow_yield, self.allow_await).parse(cursor, interner)?;
cursor.next(interner).expect("token disappeared"); SuperCall::new(args).into()
let args = Arguments::new(self.allow_yield, self.allow_await) } else {
.parse(cursor, interner)?; let mut member = MemberExpression::new(self.name, self.allow_yield, self.allow_await)
return Ok(SuperCall::new(args).into()); .parse(cursor, interner)?;
} if let Some(tok) = cursor.peek(0, interner)? {
if tok.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) {
member = CallExpression::new(self.allow_yield, self.allow_await, member)
.parse(cursor, interner)?;
} }
} }
} member
};
// TODO: Implement NewExpression: new MemberExpression
let lhs = MemberExpression::new(self.name, self.allow_yield, self.allow_await)
.parse(cursor, interner)?;
if let Some(tok) = cursor.peek(0, interner)? { if let Some(tok) = cursor.peek(0, interner)? {
if tok.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { if tok.kind() == &TokenKind::Punctuator(Punctuator::Optional) {
return CallExpression::new(self.allow_yield, self.allow_await, lhs) lhs = OptionalExpression::new(self.allow_yield, self.allow_await, lhs)
.parse(cursor, interner); .parse(cursor, interner)?
.into();
} }
} }
Ok(lhs) Ok(lhs)
} }
} }

168
boa_engine/src/syntax/parser/expression/left_hand_side/optional/mod.rs

@ -0,0 +1,168 @@
#[cfg(test)]
mod tests;
use std::io::Read;
use boa_interner::{Interner, Sym};
use boa_profiler::Profiler;
use crate::syntax::{
ast::{
self,
expression::{
access::PropertyAccessField, Optional, OptionalOperation, OptionalOperationKind,
},
Punctuator,
},
lexer::{Token, TokenKind},
parser::{
cursor::Cursor, expression::Expression, AllowAwait, AllowYield, ParseError, ParseResult,
TokenParser,
},
};
use super::arguments::Arguments;
/// Parses an optional expression.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript specification][spec]
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
/// [spec]: https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-OptionalExpression
#[derive(Debug, Clone)]
pub(in crate::syntax::parser) struct OptionalExpression {
allow_yield: AllowYield,
allow_await: AllowAwait,
target: ast::Expression,
}
impl OptionalExpression {
/// Creates a new `OptionalExpression` parser.
pub(in crate::syntax::parser) fn new<Y, A>(
allow_yield: Y,
allow_await: A,
target: ast::Expression,
) -> Self
where
Y: Into<AllowYield>,
A: Into<AllowAwait>,
{
Self {
allow_yield: allow_yield.into(),
allow_await: allow_await.into(),
target,
}
}
}
impl<R> TokenParser<R> for OptionalExpression
where
R: Read,
{
type Output = Optional;
fn parse(self, cursor: &mut Cursor<R>, interner: &mut Interner) -> ParseResult<Self::Output> {
fn parse_const_access<R: Read>(
cursor: &mut Cursor<R>,
token: &Token,
interner: &mut Interner,
) -> ParseResult<OptionalOperationKind> {
let item = match token.kind() {
TokenKind::Identifier(name) => OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(*name),
},
TokenKind::Keyword((kw, _)) => OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(kw.to_sym(interner)),
},
TokenKind::BooleanLiteral(true) => OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(Sym::TRUE),
},
TokenKind::BooleanLiteral(false) => OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(Sym::FALSE),
},
TokenKind::NullLiteral => OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(Sym::NULL),
},
TokenKind::PrivateIdentifier(name) => {
cursor.push_used_private_identifier(*name, token.span().start())?;
OptionalOperationKind::PrivatePropertyAccess { field: *name }
}
_ => {
return Err(ParseError::expected(
["identifier".to_owned()],
token.to_string(interner),
token.span(),
"optional chain",
))
}
};
Ok(item)
}
let _timer = Profiler::global().start_event("OptionalExpression", "Parsing");
let mut items = Vec::new();
while let Some(token) = cursor.peek(0, interner)? {
let shorted = match token.kind() {
TokenKind::Punctuator(Punctuator::Optional) => {
cursor.next(interner).expect("token disappeared");
true
}
TokenKind::Punctuator(Punctuator::OpenParen | Punctuator::OpenBracket) => false,
TokenKind::Punctuator(Punctuator::Dot) => {
cursor.next(interner).expect("token disappeared");
let field = cursor.next(interner)?.ok_or(ParseError::AbruptEnd)?;
let item = parse_const_access(cursor, &field, interner)?;
items.push(OptionalOperation::new(item, false));
continue;
}
TokenKind::TemplateMiddle(_) | TokenKind::TemplateNoSubstitution(_) => {
return Err(ParseError::general(
"Invalid tagged template on optional chain",
token.span().start(),
))
}
_ => break,
};
let token = cursor.peek(0, interner)?.ok_or(ParseError::AbruptEnd)?;
let item = match token.kind() {
TokenKind::Punctuator(Punctuator::OpenParen) => {
let args = Arguments::new(self.allow_yield, self.allow_await)
.parse(cursor, interner)?;
OptionalOperationKind::Call { args }
}
TokenKind::Punctuator(Punctuator::OpenBracket) => {
cursor
.next(interner)?
.expect("open bracket punctuator token disappeared"); // We move the parser forward.
let idx = Expression::new(None, true, self.allow_yield, self.allow_await)
.parse(cursor, interner)?;
cursor.expect(Punctuator::CloseBracket, "optional chain", interner)?;
OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Expr(Box::new(idx)),
}
}
TokenKind::TemplateMiddle(_) | TokenKind::TemplateNoSubstitution(_) => {
return Err(ParseError::general(
"Invalid tagged template on optional chain",
token.span().start(),
))
}
_ => {
let token = cursor.next(interner)?.expect("token disappeared");
parse_const_access(cursor, &token, interner)?
}
};
items.push(OptionalOperation::new(item, shorted));
}
Ok(Optional::new(self.target, items.into()))
}
}

93
boa_engine/src/syntax/parser/expression/left_hand_side/optional/tests.rs

@ -0,0 +1,93 @@
use boa_interner::Interner;
use boa_macros::utf16;
use crate::syntax::{
ast::{
expression::{
access::PropertyAccessField, literal::Literal, Identifier, Optional, OptionalOperation,
OptionalOperationKind,
},
Expression, Statement,
},
parser::tests::{check_invalid, check_parser},
};
#[test]
fn simple() {
let mut interner = Interner::default();
check_parser(
r#"5?.name"#,
vec![Statement::Expression(
Optional::new(
Literal::Int(5).into(),
vec![OptionalOperation::new(
OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(
interner.get_or_intern_static("name", utf16!("name")),
),
},
true,
)]
.into(),
)
.into(),
)
.into()],
interner,
);
}
#[test]
fn complex_chain() {
let mut interner = Interner::default();
check_parser(
r#"a?.b(true)?.["c"]"#,
vec![Statement::Expression(
Optional::new(
Identifier::new(interner.get_or_intern_static("a", utf16!("a"))).into(),
vec![
OptionalOperation::new(
OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Const(
interner.get_or_intern_static("b", utf16!("b")),
),
},
true,
),
OptionalOperation::new(
OptionalOperationKind::Call {
args: vec![Expression::Literal(Literal::Bool(true))].into(),
},
false,
),
OptionalOperation::new(
OptionalOperationKind::SimplePropertyAccess {
field: PropertyAccessField::Expr(Box::new(
Literal::String(interner.get_or_intern_static("c", utf16!("c")))
.into(),
)),
},
true,
),
]
.into(),
)
.into(),
)
.into()],
interner,
);
}
#[test]
fn reject_templates() {
check_invalid("console.log?.`Hello`");
check_invalid("console?.log`Hello`");
check_invalid(
r#"
const a = console?.log
`Hello`"#,
);
}

6
boa_engine/src/vm/code_block.rs

@ -169,6 +169,11 @@ impl CodeBlock {
let opcode: Opcode = self.code[*pc].try_into().expect("invalid opcode"); let opcode: Opcode = self.code[*pc].try_into().expect("invalid opcode");
*pc += size_of::<Opcode>(); *pc += size_of::<Opcode>();
match opcode { match opcode {
Opcode::RotateLeft | Opcode::RotateRight => {
let result = self.read::<u8>(*pc).to_string();
*pc += size_of::<u8>();
result
}
Opcode::PushInt8 => { Opcode::PushInt8 => {
let result = self.read::<i8>(*pc).to_string(); let result = self.read::<i8>(*pc).to_string();
*pc += size_of::<i8>(); *pc += size_of::<i8>();
@ -193,6 +198,7 @@ impl CodeBlock {
| Opcode::Jump | Opcode::Jump
| Opcode::JumpIfFalse | Opcode::JumpIfFalse
| Opcode::JumpIfNotUndefined | Opcode::JumpIfNotUndefined
| Opcode::JumpIfNullOrUndefined
| Opcode::CatchStart | Opcode::CatchStart
| Opcode::FinallySetJump | Opcode::FinallySetJump
| Opcode::Case | Opcode::Case

3
boa_engine/src/vm/mod.rs

@ -132,6 +132,8 @@ impl Context {
Opcode::PopIfThrown => PopIfThrown::execute(self)?, Opcode::PopIfThrown => PopIfThrown::execute(self)?,
Opcode::Dup => Dup::execute(self)?, Opcode::Dup => Dup::execute(self)?,
Opcode::Swap => Swap::execute(self)?, Opcode::Swap => Swap::execute(self)?,
Opcode::RotateLeft => RotateLeft::execute(self)?,
Opcode::RotateRight => RotateRight::execute(self)?,
Opcode::PushUndefined => PushUndefined::execute(self)?, Opcode::PushUndefined => PushUndefined::execute(self)?,
Opcode::PushNull => PushNull::execute(self)?, Opcode::PushNull => PushNull::execute(self)?,
Opcode::PushTrue => PushTrue::execute(self)?, Opcode::PushTrue => PushTrue::execute(self)?,
@ -198,6 +200,7 @@ impl Context {
Opcode::Jump => Jump::execute(self)?, Opcode::Jump => Jump::execute(self)?,
Opcode::JumpIfFalse => JumpIfFalse::execute(self)?, Opcode::JumpIfFalse => JumpIfFalse::execute(self)?,
Opcode::JumpIfNotUndefined => JumpIfNotUndefined::execute(self)?, Opcode::JumpIfNotUndefined => JumpIfNotUndefined::execute(self)?,
Opcode::JumpIfNullOrUndefined => JumpIfNullOrUndefined::execute(self)?,
Opcode::LogicalAnd => LogicalAnd::execute(self)?, Opcode::LogicalAnd => LogicalAnd::execute(self)?,
Opcode::LogicalOr => LogicalOr::execute(self)?, Opcode::LogicalOr => LogicalOr::execute(self)?,
Opcode::Coalesce => Coalesce::execute(self)?, Opcode::Coalesce => Coalesce::execute(self)?,

22
boa_engine/src/vm/opcode/jump/mod.rs

@ -62,3 +62,25 @@ impl Operation for JumpIfNotUndefined {
Ok(ShouldExit::False) Ok(ShouldExit::False)
} }
} }
/// `JumpIfUndefined` implements the Opcode Operation for `Opcode::JumpIfUndefined`
///
/// Operation:
/// - Conditional jump to address.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct JumpIfNullOrUndefined;
impl Operation for JumpIfNullOrUndefined {
const NAME: &'static str = "JumpIfNullOrUndefined";
const INSTRUCTION: &'static str = "INST - JumpIfNullOrUndefined";
fn execute(context: &mut Context) -> JsResult<ShouldExit> {
let address = context.vm.read::<u32>();
let value = context.vm.pop();
if value.is_null_or_undefined() {
context.vm.frame_mut().pc = address as usize;
}
context.vm.push(value);
Ok(ShouldExit::False)
}
}

41
boa_engine/src/vm/opcode/mod.rs

@ -137,6 +137,26 @@ pub enum Opcode {
/// Stack: second, first **=>** first, second /// Stack: second, first **=>** first, second
Swap, Swap,
/// Rotates the top `n` values of the stack to the left by `1`.
///
/// Equivalent to calling [`slice::rotate_left`] with argument `1` on the top `n` values of the
/// stack.
///
/// Operands: n: `u8`
///
/// Stack: v\[n\], v\[n-1\], ... , v\[1\], v\[0\] **=>** v\[n-1\], ... , v\[1\], v\[0\], v\[n\]
RotateLeft,
/// Rotates the top `n` values of the stack to the right by `1`.
///
/// Equivalent to calling [`slice::rotate_right`] with argument `1` on the top `n` values of the
/// stack.
///
/// Operands: n: `u8`
///
/// Stack: v\[n\], v\[n-1\], ... , v\[1\], v\[0\] **=>** v\[0\], v\[n\], v\[n-1\], ... , v\[1\]
RotateRight,
/// Push integer `0` on the stack. /// Push integer `0` on the stack.
/// ///
/// Operands: /// Operands:
@ -902,6 +922,15 @@ pub enum Opcode {
/// Stack: value **=>** value /// Stack: value **=>** value
JumpIfNotUndefined, JumpIfNotUndefined,
/// Conditional jump to address.
///
/// If the value popped is undefined jump to `address`.
///
/// Operands: address: `u32`
///
/// Stack: value **=>** value
JumpIfNullOrUndefined,
/// Throw exception /// Throw exception
/// ///
/// Operands: /// Operands:
@ -1061,14 +1090,14 @@ pub enum Opcode {
/// ///
/// Operands: argument_count: `u32` /// Operands: argument_count: `u32`
/// ///
/// Stack: func, this, argument_1, ... argument_n **=>** result /// Stack: this, func, argument_1, ... argument_n **=>** result
CallEval, CallEval,
/// Call a function named "eval" where the arguments contain spreads. /// Call a function named "eval" where the arguments contain spreads.
/// ///
/// Operands: /// Operands:
/// ///
/// Stack: arguments_array, func, this **=>** result /// Stack: this, func, arguments_array **=>** result
CallEvalSpread, CallEvalSpread,
/// Call a function. /// Call a function.
@ -1082,7 +1111,7 @@ pub enum Opcode {
/// ///
/// Operands: /// Operands:
/// ///
/// Stack: arguments_array, func, this **=>** result /// Stack: this, func, arguments_array **=>** result
CallSpread, CallSpread,
/// Call construct on a function. /// Call construct on a function.
@ -1330,6 +1359,8 @@ impl Opcode {
Self::PopIfThrown => PopIfThrown::NAME, Self::PopIfThrown => PopIfThrown::NAME,
Self::Dup => Dup::NAME, Self::Dup => Dup::NAME,
Self::Swap => Swap::NAME, Self::Swap => Swap::NAME,
Self::RotateLeft => RotateLeft::NAME,
Self::RotateRight => RotateRight::NAME,
Self::PushZero => PushZero::NAME, Self::PushZero => PushZero::NAME,
Self::PushOne => PushOne::NAME, Self::PushOne => PushOne::NAME,
Self::PushInt8 => PushInt8::NAME, Self::PushInt8 => PushInt8::NAME,
@ -1431,6 +1462,7 @@ impl Opcode {
Self::Jump => Jump::NAME, Self::Jump => Jump::NAME,
Self::JumpIfFalse => JumpIfFalse::NAME, Self::JumpIfFalse => JumpIfFalse::NAME,
Self::JumpIfNotUndefined => JumpIfNotUndefined::NAME, Self::JumpIfNotUndefined => JumpIfNotUndefined::NAME,
Self::JumpIfNullOrUndefined => JumpIfNullOrUndefined::NAME,
Self::Throw => Throw::NAME, Self::Throw => Throw::NAME,
Self::TryStart => TryStart::NAME, Self::TryStart => TryStart::NAME,
Self::TryEnd => TryEnd::NAME, Self::TryEnd => TryEnd::NAME,
@ -1499,6 +1531,8 @@ impl Opcode {
Self::PopIfThrown => PopIfThrown::INSTRUCTION, Self::PopIfThrown => PopIfThrown::INSTRUCTION,
Self::Dup => Dup::INSTRUCTION, Self::Dup => Dup::INSTRUCTION,
Self::Swap => Swap::INSTRUCTION, Self::Swap => Swap::INSTRUCTION,
Self::RotateLeft => RotateLeft::INSTRUCTION,
Self::RotateRight => RotateRight::INSTRUCTION,
Self::PushZero => PushZero::INSTRUCTION, Self::PushZero => PushZero::INSTRUCTION,
Self::PushOne => PushOne::INSTRUCTION, Self::PushOne => PushOne::INSTRUCTION,
Self::PushInt8 => PushInt8::INSTRUCTION, Self::PushInt8 => PushInt8::INSTRUCTION,
@ -1579,6 +1613,7 @@ impl Opcode {
Self::Jump => Jump::INSTRUCTION, Self::Jump => Jump::INSTRUCTION,
Self::JumpIfFalse => JumpIfFalse::INSTRUCTION, Self::JumpIfFalse => JumpIfFalse::INSTRUCTION,
Self::JumpIfNotUndefined => JumpIfNotUndefined::INSTRUCTION, Self::JumpIfNotUndefined => JumpIfNotUndefined::INSTRUCTION,
Self::JumpIfNullOrUndefined => JumpIfNullOrUndefined::INSTRUCTION,
Self::Throw => Throw::INSTRUCTION, Self::Throw => Throw::INSTRUCTION,
Self::TryStart => TryStart::INSTRUCTION, Self::TryStart => TryStart::INSTRUCTION,
Self::TryEnd => TryEnd::INSTRUCTION, Self::TryEnd => TryEnd::INSTRUCTION,

38
boa_engine/src/vm/opcode/swap/mod.rs

@ -23,3 +23,41 @@ impl Operation for Swap {
Ok(ShouldExit::False) Ok(ShouldExit::False)
} }
} }
/// `RotateLeft` implements the Opcode Operation for `Opcode::RotateLeft`
///
/// Operation:
/// - Rotates the n top values to the left.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct RotateLeft;
impl Operation for RotateLeft {
const NAME: &'static str = "RotateLeft";
const INSTRUCTION: &'static str = "INST - RotateLeft";
fn execute(context: &mut Context) -> JsResult<ShouldExit> {
let n = context.vm.read::<u8>() as usize;
let len = context.vm.stack.len();
context.vm.stack[(len - n)..].rotate_left(1);
Ok(ShouldExit::False)
}
}
/// `RotateRight` implements the Opcode Operation for `Opcode::RotateRight`
///
/// Operation:
/// - Rotates the n top values to the right.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct RotateRight;
impl Operation for RotateRight {
const NAME: &'static str = "RotateRight";
const INSTRUCTION: &'static str = "INST - RotateRight";
fn execute(context: &mut Context) -> JsResult<ShouldExit> {
let n = context.vm.read::<u8>() as usize;
let len = context.vm.stack.len();
context.vm.stack[(len - n)..].rotate_right(1);
Ok(ShouldExit::False)
}
}

Loading…
Cancel
Save