diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 0549587811..28ad4eff42 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -12,7 +12,7 @@ use crate::{ binary::{ArithmeticOp, BinaryOp, BitwiseOp, LogicalOp, RelationalOp}, unary::UnaryOp, }, - Call, Identifier, New, + Call, Identifier, New, Optional, OptionalOperationKind, }, function::{ ArrowFunction, AsyncFunction, AsyncGenerator, Class, ClassElement, FormalParameterList, @@ -430,6 +430,14 @@ impl<'b> ByteCompiler<'b> { 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. /// Return the `Label` of the operand. #[inline] @@ -1109,6 +1117,7 @@ impl<'b> ByteCompiler<'b> { } }, PropertyDefinition::MethodDefinition(name, kind) => match kind { + // TODO: set function name for getter and setters MethodDefinition::Get(expr) => match name { PropertyName::Literal(name) => { self.function(expr.into(), NodeKind::Expression, true)?; @@ -1123,6 +1132,7 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::SetPropertyGetterByValue); } }, + // TODO: set function name for getter and setters MethodDefinition::Set(expr) => match name { PropertyName::Literal(name) => { self.function(expr.into(), NodeKind::Expression, true)?; @@ -1443,12 +1453,201 @@ impl<'b> ByteCompiler<'b> { 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 Expression::FormalParameterList(_) => unreachable!(), } 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<()> { for variable in decl.0.as_ref() { match variable.binding() { @@ -2292,7 +2491,6 @@ impl<'b> ByteCompiler<'b> { function.is_async(), function.is_arrow(), ); - let FunctionSpec { name, parameters, @@ -2357,50 +2555,13 @@ impl<'b> ByteCompiler<'b> { }; match call.function() { - Expression::PropertyAccess(access) => match access { - PropertyAccess::Simple(access) => { - self.compile_expr(access.target(), true)?; - if kind == CallKind::Call { - self.emit(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::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); - } - } - } - }, + Expression::PropertyAccess(access) if kind == CallKind::Call => { + self.compile_access_preserve_this(access)?; + } + + Expression::Optional(opt) if kind == CallKind::Call => { + self.compile_optional_preserve_this(opt)?; + } expr => { self.compile_expr(expr, true)?; if kind == CallKind::Call || kind == CallKind::CallEval { @@ -2958,6 +3119,7 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::SetClassPrototype); self.emit_opcode(Opcode::Swap); + // TODO: set function name for getter and setters for element in class.elements() { match element { ClassElement::StaticMethodDefinition(name, method_definition) => { @@ -3049,6 +3211,7 @@ impl<'b> ByteCompiler<'b> { }, } } + // TODO: set names for private methods ClassElement::PrivateStaticMethodDefinition(name, method_definition) => { self.emit_opcode(Opcode::Dup); match method_definition { @@ -3218,6 +3381,7 @@ impl<'b> ByteCompiler<'b> { self.emit(Opcode::Call, &[0]); self.emit_opcode(Opcode::Pop); } + // TODO: set names for private methods ClassElement::PrivateMethodDefinition(name, method_definition) => { self.emit_opcode(Opcode::Dup); match method_definition { @@ -3263,6 +3427,7 @@ impl<'b> ByteCompiler<'b> { match element { ClassElement::MethodDefinition(name, method_definition) => { self.emit_opcode(Opcode::Dup); + // TODO: set names for getters and setters match method_definition { MethodDefinition::Get(expr) => match name { PropertyName::Literal(name) => { diff --git a/boa_engine/src/syntax/ast/expression/mod.rs b/boa_engine/src/syntax/ast/expression/mod.rs index 0ca580eee7..5e833abfd3 100644 --- a/boa_engine/src/syntax/ast/expression/mod.rs +++ b/boa_engine/src/syntax/ast/expression/mod.rs @@ -27,6 +27,7 @@ mod r#await; mod call; mod identifier; mod new; +mod optional; mod spread; mod tagged_template; mod r#yield; @@ -34,6 +35,7 @@ mod r#yield; pub use call::{Call, SuperCall}; pub use identifier::Identifier; pub use new::New; +pub use optional::{Optional, OptionalOperation, OptionalOperationKind}; pub use r#await::Await; pub use r#yield::Yield; pub use spread::Spread; @@ -110,10 +112,11 @@ pub enum Expression { /// See [`Call`]. Call(Call), - /// See [`SuperCall`] + /// See [`SuperCall`]. SuperCall(SuperCall), - // TODO: Optional chains + /// See [`Optional`]. + Optional(Optional), // TODO: Import calls /// See [`TaggedTemplate`]. @@ -173,6 +176,7 @@ impl Expression { Self::New(new) => new.to_interned_string(interner), Self::Call(call) => call.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::TaggedTemplate(tag) => tag.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::Call(call) => call.contains_arguments(), Expression::SuperCall(call) => call.contains_arguments(), + Expression::Optional(opt) => opt.contains_arguments(), Expression::TaggedTemplate(tag) => tag.contains_arguments(), Expression::Assign(assign) => assign.contains_arguments(), Expression::Unary(unary) => unary.contains_arguments(), @@ -252,6 +257,7 @@ impl Expression { Expression::Call(call) => call.contains(symbol), Expression::SuperCall(_) if symbol == ContainsSymbol::SuperCall => true, Expression::SuperCall(expr) => expr.contains(symbol), + Expression::Optional(opt) => opt.contains(symbol), Expression::TaggedTemplate(temp) => temp.contains(symbol), Expression::NewTarget => symbol == ContainsSymbol::NewTarget, Expression::Assign(assign) => assign.contains(symbol), diff --git a/boa_engine/src/syntax/ast/expression/optional.rs b/boa_engine/src/syntax/ast/expression/optional.rs new file mode 100644 index 0000000000..11a56b1e8f --- /dev/null +++ b/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, + 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 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 + } +} diff --git a/boa_engine/src/syntax/ast/punctuator.rs b/boa_engine/src/syntax/ast/punctuator.rs index f1e952ac4b..0f678d9948 100644 --- a/boa_engine/src/syntax/ast/punctuator.rs +++ b/boa_engine/src/syntax/ast/punctuator.rs @@ -113,6 +113,8 @@ pub enum Punctuator { OpenBracket, /// `(` OpenParen, + /// `?.` + Optional, /// `|` Or, /// `**` @@ -243,6 +245,7 @@ impl Punctuator { Self::OpenBlock => "{", Self::OpenBracket => "[", Self::OpenParen => "(", + Self::Optional => "?.", Self::Or => "|", Self::Exp => "**", Self::Question => "?", diff --git a/boa_engine/src/syntax/lexer/cursor.rs b/boa_engine/src/syntax/lexer/cursor.rs index 0b38fd8713..e59d0e8e15 100644 --- a/boa_engine/src/syntax/lexer/cursor.rs +++ b/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). #[inline] - pub(super) fn peek_n(&mut self, n: u8) -> Result { + pub(super) fn peek_n(&mut self, n: u8) -> Result<&[u8], Error> { let _timer = Profiler::global().start_event("cursor::peek_n()", "Lexing"); self.iter.peek_n_bytes(n) @@ -250,7 +250,7 @@ where Some(0xE2) => { // Try to match '\u{2028}' (e2 80 a8) and '\u{2029}' (e2 80 a9) 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(); } else { // 0xE2 is a utf8 first byte @@ -296,7 +296,7 @@ where struct InnerIter { iter: Bytes, num_peeked_bytes: u8, - peeked_bytes: u32, + peeked_bytes: [u8; 4], peeked_char: Option>, } @@ -307,7 +307,7 @@ impl InnerIter { Self { iter, num_peeked_bytes: 0, - peeked_bytes: 0, + peeked_bytes: [0; 4], peeked_char: None, } } @@ -349,13 +349,13 @@ where #[inline] pub(super) fn peek_byte(&mut self) -> Result, Error> { if self.num_peeked_bytes > 0 { - let byte = self.peeked_bytes as u8; + let byte = self.peeked_bytes[0]; Ok(Some(byte)) } else { match self.iter.next().transpose()? { Some(byte) => { self.num_peeked_bytes = 1; - self.peeked_bytes = u32::from(byte); + self.peeked_bytes[0] = byte; Ok(Some(byte)) } None => Ok(None), @@ -365,24 +365,17 @@ where /// Peeks the next n bytes, the maximum number of peeked bytes is 4 (n <= 4). #[inline] - pub(super) fn peek_n_bytes(&mut self, n: u8) -> Result { + pub(super) fn peek_n_bytes(&mut self, n: u8) -> Result<&[u8], Error> { while self.num_peeked_bytes < n && self.num_peeked_bytes < 4 { match self.iter.next().transpose()? { 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; } None => break, }; } - - 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), - } + Ok(&self.peeked_bytes[..usize::from(u8::min(n, self.num_peeked_bytes))]) } /// Peeks the next unchecked character in u32 code point. @@ -392,34 +385,40 @@ where Ok(ch) } else { // Decode UTF-8 - let x = match self.peek_byte()? { - Some(b) if b < 128 => { - self.peeked_char = Some(Some(u32::from(b))); - return Ok(Some(u32::from(b))); + let (x, y, z, w) = match self.peek_n_bytes(4)? { + [b, ..] if *b < 128 => { + let char = u32::from(*b); + self.peeked_char = Some(Some(char)); + return Ok(Some(char)); } - Some(b) => b, - None => { + [] => { self.peeked_char = None; return Ok(None); } + bytes => ( + bytes[0], + bytes.get(1).copied(), + bytes.get(2).copied(), + bytes.get(3).copied(), + ), }; // Multibyte case follows // Decode from a byte combination out of: [[[x y] z] w] // NOTE: Performance is sensitive to the exact formulation here 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); if x >= 0xE0 { // [[x y z] w] case // 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); ch = init << 12 | y_z; if x >= 0xF0 { // [x y z w] case // 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); } }; @@ -434,9 +433,9 @@ where fn next_byte(&mut self) -> io::Result> { self.peeked_char = None; 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.peeked_bytes >>= 8; + self.peeked_bytes.rotate_left(1); Ok(Some(byte)) } else { self.iter.next().transpose() diff --git a/boa_engine/src/syntax/lexer/identifier.rs b/boa_engine/src/syntax/lexer/identifier.rs index e57c42dcfa..e44d697e3e 100644 --- a/boa_engine/src/syntax/lexer/identifier.rs +++ b/boa_engine/src/syntax/lexer/identifier.rs @@ -124,7 +124,7 @@ impl Identifier { loop { 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 _next = cursor.next_byte(); let _next = cursor.next_byte(); diff --git a/boa_engine/src/syntax/lexer/operator.rs b/boa_engine/src/syntax/lexer/operator.rs index 122152354f..f53d85bfb7 100644 --- a/boa_engine/src/syntax/lexer/operator.rs +++ b/boa_engine/src/syntax/lexer/operator.rs @@ -126,21 +126,34 @@ impl Tokenizer for Operator { b'&' => op!(cursor, start_pos, Ok(Punctuator::AssignAnd), Ok(Punctuator::And), { Some(b'&') => vop!(cursor, Ok(Punctuator::AssignBoolAnd), 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) - ) + b'?' => { + let (first, second) = ( + cursor.peek_n(2)?.first().copied(), + cursor.peek_n(2)?.get(1).copied(), + ); + match first { + Some(b'?') => { + 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!( cursor, start_pos, diff --git a/boa_engine/src/syntax/lexer/regex.rs b/boa_engine/src/syntax/lexer/regex.rs index 5f695f4260..0e2e9e0018 100644 --- a/boa_engine/src/syntax/lexer/regex.rs +++ b/boa_engine/src/syntax/lexer/regex.rs @@ -71,7 +71,9 @@ impl Tokenizer for RegexLiteral { 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 return Err(Error::syntax( "new lines are not allowed in regular expressions", @@ -90,8 +92,8 @@ impl Tokenizer for RegexLiteral { 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 return Err(Error::syntax( diff --git a/boa_engine/src/syntax/lexer/tests.rs b/boa_engine/src/syntax/lexer/tests.rs index 13db696c14..12bc197e46 100644 --- a/boa_engine/src/syntax/lexer/tests.rs +++ b/boa_engine/src/syntax/lexer/tests.rs @@ -182,7 +182,7 @@ fn check_punctuators() { // https://tc39.es/ecma262/#sec-punctuators let s = "{ ( ) [ ] . ... ; , < > <= >= == != === !== \ + - * % -- << >> >>> & | ^ ! ~ && || ? : \ - = += -= *= &= **= ++ ** <<= >>= >>>= &= |= ^= => ?? ??= &&= ||="; + = += -= *= &= **= ++ ** <<= >>= >>>= &= |= ^= => ?? ??= &&= ||= ?."; let mut lexer = Lexer::new(s.as_bytes()); let mut interner = Interner::default(); @@ -240,6 +240,7 @@ fn check_punctuators() { TokenKind::Punctuator(Punctuator::AssignCoalesce), TokenKind::Punctuator(Punctuator::AssignBoolAnd), TokenKind::Punctuator(Punctuator::AssignBoolOr), + TokenKind::Punctuator(Punctuator::Optional), ]; expect_tokens(&mut lexer, &expected, &mut interner); diff --git a/boa_engine/src/syntax/parser/expression/left_hand_side/mod.rs b/boa_engine/src/syntax/parser/expression/left_hand_side/mod.rs index c06531ed5a..5d6389b2ea 100644 --- a/boa_engine/src/syntax/parser/expression/left_hand_side/mod.rs +++ b/boa_engine/src/syntax/parser/expression/left_hand_side/mod.rs @@ -13,6 +13,7 @@ mod tests; mod arguments; mod call; mod member; +mod optional; mod template; use crate::syntax::{ @@ -24,6 +25,7 @@ use crate::syntax::{ parser::{ expression::left_hand_side::{ arguments::Arguments, call::CallExpression, member::MemberExpression, + optional::OptionalExpression, }, AllowAwait, AllowYield, Cursor, ParseResult, TokenParser, }, @@ -70,32 +72,59 @@ where type Output = Expression; fn parse(self, cursor: &mut Cursor, interner: &mut Interner) -> ParseResult { + /// 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( + cursor: &mut Cursor, + interner: &mut Interner, + ) -> ParseResult { + 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"); cursor.set_goal(InputElement::TemplateTail); - 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) { - cursor.next(interner).expect("token disappeared"); - let args = Arguments::new(self.allow_yield, self.allow_await) - .parse(cursor, interner)?; - return Ok(SuperCall::new(args).into()); - } + let mut lhs = if is_super_call(cursor, interner)? { + cursor.next(interner).expect("token disappeared"); + let args = + Arguments::new(self.allow_yield, self.allow_await).parse(cursor, interner)?; + SuperCall::new(args).into() + } else { + let mut member = MemberExpression::new(self.name, self.allow_yield, self.allow_await) + .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 tok.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { - return CallExpression::new(self.allow_yield, self.allow_await, lhs) - .parse(cursor, interner); + if tok.kind() == &TokenKind::Punctuator(Punctuator::Optional) { + lhs = OptionalExpression::new(self.allow_yield, self.allow_await, lhs) + .parse(cursor, interner)? + .into(); } } + Ok(lhs) } } diff --git a/boa_engine/src/syntax/parser/expression/left_hand_side/optional/mod.rs b/boa_engine/src/syntax/parser/expression/left_hand_side/optional/mod.rs new file mode 100644 index 0000000000..ce78197969 --- /dev/null +++ b/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( + allow_yield: Y, + allow_await: A, + target: ast::Expression, + ) -> Self + where + Y: Into, + A: Into, + { + Self { + allow_yield: allow_yield.into(), + allow_await: allow_await.into(), + target, + } + } +} + +impl TokenParser for OptionalExpression +where + R: Read, +{ + type Output = Optional; + + fn parse(self, cursor: &mut Cursor, interner: &mut Interner) -> ParseResult { + fn parse_const_access( + cursor: &mut Cursor, + token: &Token, + interner: &mut Interner, + ) -> ParseResult { + 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())) + } +} diff --git a/boa_engine/src/syntax/parser/expression/left_hand_side/optional/tests.rs b/boa_engine/src/syntax/parser/expression/left_hand_side/optional/tests.rs new file mode 100644 index 0000000000..b7a5988808 --- /dev/null +++ b/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`"#, + ); +} diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index f3a148bd0d..e434e41aef 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -169,6 +169,11 @@ impl CodeBlock { let opcode: Opcode = self.code[*pc].try_into().expect("invalid opcode"); *pc += size_of::(); match opcode { + Opcode::RotateLeft | Opcode::RotateRight => { + let result = self.read::(*pc).to_string(); + *pc += size_of::(); + result + } Opcode::PushInt8 => { let result = self.read::(*pc).to_string(); *pc += size_of::(); @@ -193,6 +198,7 @@ impl CodeBlock { | Opcode::Jump | Opcode::JumpIfFalse | Opcode::JumpIfNotUndefined + | Opcode::JumpIfNullOrUndefined | Opcode::CatchStart | Opcode::FinallySetJump | Opcode::Case diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index f90c2f0c50..66d5c9f448 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -132,6 +132,8 @@ impl Context { Opcode::PopIfThrown => PopIfThrown::execute(self)?, Opcode::Dup => Dup::execute(self)?, Opcode::Swap => Swap::execute(self)?, + Opcode::RotateLeft => RotateLeft::execute(self)?, + Opcode::RotateRight => RotateRight::execute(self)?, Opcode::PushUndefined => PushUndefined::execute(self)?, Opcode::PushNull => PushNull::execute(self)?, Opcode::PushTrue => PushTrue::execute(self)?, @@ -198,6 +200,7 @@ impl Context { Opcode::Jump => Jump::execute(self)?, Opcode::JumpIfFalse => JumpIfFalse::execute(self)?, Opcode::JumpIfNotUndefined => JumpIfNotUndefined::execute(self)?, + Opcode::JumpIfNullOrUndefined => JumpIfNullOrUndefined::execute(self)?, Opcode::LogicalAnd => LogicalAnd::execute(self)?, Opcode::LogicalOr => LogicalOr::execute(self)?, Opcode::Coalesce => Coalesce::execute(self)?, diff --git a/boa_engine/src/vm/opcode/jump/mod.rs b/boa_engine/src/vm/opcode/jump/mod.rs index f509a59c97..19a1e5db5f 100644 --- a/boa_engine/src/vm/opcode/jump/mod.rs +++ b/boa_engine/src/vm/opcode/jump/mod.rs @@ -62,3 +62,25 @@ impl Operation for JumpIfNotUndefined { 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 { + let address = context.vm.read::(); + 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) + } +} diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index a8d7af62b8..ecf9aa3e34 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -137,6 +137,26 @@ pub enum Opcode { /// Stack: second, first **=>** first, second 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. /// /// Operands: @@ -902,6 +922,15 @@ pub enum Opcode { /// Stack: value **=>** value JumpIfNotUndefined, + /// Conditional jump to address. + /// + /// If the value popped is undefined jump to `address`. + /// + /// Operands: address: `u32` + /// + /// Stack: value **=>** value + JumpIfNullOrUndefined, + /// Throw exception /// /// Operands: @@ -1061,14 +1090,14 @@ pub enum Opcode { /// /// Operands: argument_count: `u32` /// - /// Stack: func, this, argument_1, ... argument_n **=>** result + /// Stack: this, func, argument_1, ... argument_n **=>** result CallEval, /// Call a function named "eval" where the arguments contain spreads. /// /// Operands: /// - /// Stack: arguments_array, func, this **=>** result + /// Stack: this, func, arguments_array **=>** result CallEvalSpread, /// Call a function. @@ -1082,7 +1111,7 @@ pub enum Opcode { /// /// Operands: /// - /// Stack: arguments_array, func, this **=>** result + /// Stack: this, func, arguments_array **=>** result CallSpread, /// Call construct on a function. @@ -1330,6 +1359,8 @@ impl Opcode { Self::PopIfThrown => PopIfThrown::NAME, Self::Dup => Dup::NAME, Self::Swap => Swap::NAME, + Self::RotateLeft => RotateLeft::NAME, + Self::RotateRight => RotateRight::NAME, Self::PushZero => PushZero::NAME, Self::PushOne => PushOne::NAME, Self::PushInt8 => PushInt8::NAME, @@ -1431,6 +1462,7 @@ impl Opcode { Self::Jump => Jump::NAME, Self::JumpIfFalse => JumpIfFalse::NAME, Self::JumpIfNotUndefined => JumpIfNotUndefined::NAME, + Self::JumpIfNullOrUndefined => JumpIfNullOrUndefined::NAME, Self::Throw => Throw::NAME, Self::TryStart => TryStart::NAME, Self::TryEnd => TryEnd::NAME, @@ -1499,6 +1531,8 @@ impl Opcode { Self::PopIfThrown => PopIfThrown::INSTRUCTION, Self::Dup => Dup::INSTRUCTION, Self::Swap => Swap::INSTRUCTION, + Self::RotateLeft => RotateLeft::INSTRUCTION, + Self::RotateRight => RotateRight::INSTRUCTION, Self::PushZero => PushZero::INSTRUCTION, Self::PushOne => PushOne::INSTRUCTION, Self::PushInt8 => PushInt8::INSTRUCTION, @@ -1579,6 +1613,7 @@ impl Opcode { Self::Jump => Jump::INSTRUCTION, Self::JumpIfFalse => JumpIfFalse::INSTRUCTION, Self::JumpIfNotUndefined => JumpIfNotUndefined::INSTRUCTION, + Self::JumpIfNullOrUndefined => JumpIfNullOrUndefined::INSTRUCTION, Self::Throw => Throw::INSTRUCTION, Self::TryStart => TryStart::INSTRUCTION, Self::TryEnd => TryEnd::INSTRUCTION, diff --git a/boa_engine/src/vm/opcode/swap/mod.rs b/boa_engine/src/vm/opcode/swap/mod.rs index fa74ca5ede..941812af60 100644 --- a/boa_engine/src/vm/opcode/swap/mod.rs +++ b/boa_engine/src/vm/opcode/swap/mod.rs @@ -23,3 +23,41 @@ impl Operation for Swap { 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 { + let n = context.vm.read::() 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 { + let n = context.vm.read::() as usize; + let len = context.vm.stack.len(); + context.vm.stack[(len - n)..].rotate_right(1); + Ok(ShouldExit::False) + } +}