Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/ast/src/expression/literal/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ impl ArrayLiteral {
});
}
},
// Annex B: a Call as assignment target is never a valid
// destructuring element — treat as invalid pattern.
#[cfg(feature = "annex-b")]
AssignTarget::Call(_) => return None,
}
}
Expression::ArrayLiteral(array) => {
Expand Down
4 changes: 4 additions & 0 deletions core/ast/src/expression/literal/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ impl ObjectLiteral {
default_init: Some(assign.rhs().clone()),
});
}
// Annex B: a Call as assignment target is never a valid
// destructuring element — treat as invalid pattern.
#[cfg(feature = "annex-b")]
AssignTarget::Call(_) => return None,
}
}
(_, Expression::PropertyAccess(access)) => {
Expand Down
22 changes: 21 additions & 1 deletion core/ast/src/expression/operator/assign/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use boa_interner::{Interner, Sym, ToInternedString};

use crate::{
Span, Spanned,
expression::{Expression, access::PropertyAccess, identifier::Identifier},
expression::{Call, Expression, access::PropertyAccess, identifier::Identifier},
pattern::Pattern,
visitor::{VisitWith, Visitor, VisitorMut},
};
Expand Down Expand Up @@ -130,6 +130,16 @@ pub enum AssignTarget {
Access(PropertyAccess),
/// A pattern assignment, such as `{a, b, ...c}`.
Pattern(Pattern),
/// Annex B: a call expression as LHS in non-strict code.
///
/// Per [sec-runtime-errors-for-function-call-assignment-targets], this is
/// syntactically valid in non-strict mode but always throws a `ReferenceError`
/// at runtime.
///
/// [sec-runtime-errors-for-function-call-assignment-targets]:
/// https://tc39.es/ecma262/#sec-runtime-errors-for-function-call-assignment-targets
#[cfg(feature = "annex-b")]
Call(Box<Call>),
}

impl AssignTarget {
Expand Down Expand Up @@ -165,6 +175,8 @@ impl AssignTarget {
Expression::Identifier(id) => Some(Self::Identifier(*id)),
Expression::PropertyAccess(access) => Some(Self::Access(access.clone())),
Expression::Parenthesized(p) => Self::from_expression_simple(p.expression(), strict),
#[cfg(feature = "annex-b")]
Expression::Call(call) if !strict => Some(Self::Call(Box::new(call.clone()))),
_ => None,
}
}
Expand All @@ -177,6 +189,8 @@ impl Spanned for AssignTarget {
AssignTarget::Identifier(identifier) => identifier.span(),
AssignTarget::Access(property_access) => property_access.span(),
AssignTarget::Pattern(pattern) => pattern.span(),
#[cfg(feature = "annex-b")]
AssignTarget::Call(call) => call.span(),
}
}
}
Expand All @@ -188,6 +202,8 @@ impl ToInternedString for AssignTarget {
Self::Identifier(id) => id.to_interned_string(interner),
Self::Access(access) => access.to_interned_string(interner),
Self::Pattern(pattern) => pattern.to_interned_string(interner),
#[cfg(feature = "annex-b")]
Self::Call(call) => call.to_interned_string(interner),
}
}
}
Expand All @@ -208,6 +224,8 @@ impl VisitWith for AssignTarget {
Self::Identifier(id) => visitor.visit_identifier(id),
Self::Access(pa) => visitor.visit_property_access(pa),
Self::Pattern(pat) => visitor.visit_pattern(pat),
#[cfg(feature = "annex-b")]
Self::Call(call) => visitor.visit_call(call),
}
}

Expand All @@ -219,6 +237,8 @@ impl VisitWith for AssignTarget {
Self::Identifier(id) => visitor.visit_identifier_mut(id),
Self::Access(pa) => visitor.visit_property_access_mut(pa),
Self::Pattern(pat) => visitor.visit_pattern_mut(pat),
#[cfg(feature = "annex-b")]
Self::Call(call) => visitor.visit_call_mut(call),
}
}
}
31 changes: 30 additions & 1 deletion core/ast/src/expression/operator/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod op;

use crate::{
Expression, Span, Spanned,
expression::{Identifier, access::PropertyAccess},
expression::{Identifier, Call, access::PropertyAccess},
visitor::{VisitWith, Visitor, VisitorMut},
};
use boa_interner::{Interner, ToInternedString};
Expand Down Expand Up @@ -99,6 +99,8 @@ impl VisitWith for Update {
match self.target.as_ref() {
UpdateTarget::Identifier(ident) => visitor.visit_identifier(ident),
UpdateTarget::PropertyAccess(access) => visitor.visit_property_access(access),
#[cfg(feature = "annex-b")]
UpdateTarget::Call(call) => visitor.visit_call(call),
}
}

Expand All @@ -109,6 +111,8 @@ impl VisitWith for Update {
match &mut *self.target {
UpdateTarget::Identifier(ident) => visitor.visit_identifier_mut(ident),
UpdateTarget::PropertyAccess(access) => visitor.visit_property_access_mut(access),
#[cfg(feature = "annex-b")]
UpdateTarget::Call(call) => visitor.visit_call_mut(call),
}
}
}
Expand All @@ -130,6 +134,17 @@ pub enum UpdateTarget {

/// An [`PropertyAccess`] expression.
PropertyAccess(PropertyAccess),

/// Annex B: a call expression as update target (non-strict only).
///
/// Per [sec-runtime-errors-for-function-call-assignment-targets], this is
/// syntactically valid in non-strict mode but always throws a `ReferenceError`
/// at runtime.
///
/// [sec-runtime-errors-for-function-call-assignment-targets]:
/// https://tc39.es/ecma262/#sec-runtime-errors-for-function-call-assignment-targets
#[cfg(feature = "annex-b")]
Call(Box<Call>),
}

impl ToInternedString for UpdateTarget {
Expand All @@ -138,6 +153,20 @@ impl ToInternedString for UpdateTarget {
match self {
Self::Identifier(identifier) => identifier.to_interned_string(interner),
Self::PropertyAccess(access) => access.to_interned_string(interner),
#[cfg(feature = "annex-b")]
Self::Call(call) => call.to_interned_string(interner),
}
}
}

impl Spanned for UpdateTarget {
#[inline]
fn span(&self) -> Span {
match self {
Self::Identifier(identifier) => identifier.span(),
Self::PropertyAccess(access) => access.span(),
#[cfg(feature = "annex-b")]
Self::Call(call) => call.span(),
}
}
}
18 changes: 17 additions & 1 deletion core/ast/src/statement/iteration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod while_loop;

use crate::{
declaration::{Binding, Variable},
expression::{Identifier, access::PropertyAccess},
expression::{Identifier, Call, access::PropertyAccess},
pattern::Pattern,
};
use core::ops::ControlFlow;
Expand Down Expand Up @@ -50,6 +50,16 @@ pub enum IterableLoopInitializer {
Const(Binding),
/// A pattern with already declared variables.
Pattern(Pattern),
/// Annex B: a call expression as for-in/of LHS (non-strict only).
///
/// Per [sec-runtime-errors-for-function-call-assignment-targets], this is
/// syntactically valid in non-strict mode but always throws a `ReferenceError`
/// at runtime.
///
/// [sec-runtime-errors-for-function-call-assignment-targets]:
/// https://tc39.es/ecma262/#sec-runtime-errors-for-function-call-assignment-targets
#[cfg(feature = "annex-b")]
Call(Box<Call>),
}

impl ToInternedString for IterableLoopInitializer {
Expand All @@ -58,6 +68,8 @@ impl ToInternedString for IterableLoopInitializer {
Self::Identifier(ident) => return ident.to_interned_string(interner),
Self::Pattern(pattern) => return pattern.to_interned_string(interner),
Self::Access(access) => return access.to_interned_string(interner),
#[cfg(feature = "annex-b")]
Self::Call(call) => return call.to_interned_string(interner),
Self::Var(binding) => (binding.to_interned_string(interner), "var"),
Self::Let(binding) => (binding.to_interned_string(interner), "let"),
Self::Const(binding) => (binding.to_interned_string(interner), "const"),
Expand All @@ -78,6 +90,8 @@ impl VisitWith for IterableLoopInitializer {
Self::Var(b) => visitor.visit_variable(b),
Self::Let(b) | Self::Const(b) => visitor.visit_binding(b),
Self::Pattern(p) => visitor.visit_pattern(p),
#[cfg(feature = "annex-b")]
Self::Call(call) => visitor.visit_call(call),
}
}

Expand All @@ -91,6 +105,8 @@ impl VisitWith for IterableLoopInitializer {
Self::Var(b) => visitor.visit_variable_mut(b),
Self::Let(b) | Self::Const(b) => visitor.visit_binding_mut(b),
Self::Pattern(p) => visitor.visit_pattern_mut(p),
#[cfg(feature = "annex-b")]
Self::Call(call) => visitor.visit_call_mut(call),
}
}
}
10 changes: 9 additions & 1 deletion core/engine/src/bytecompiler/expression/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use boa_ast::{
Expression,
expression::{
access::{PropertyAccess, PropertyAccessField},
operator::{Assign, assign::AssignOp},
operator::{Assign, assign::{AssignOp, AssignTarget}},
},
scope::BindingLocatorError,
};
Expand All @@ -15,6 +15,14 @@ impl ByteCompiler<'_> {
pub(crate) fn compile_assign(&mut self, assign: &Assign, dst: &Register) {
let mut compiler = self.position_guard(assign);

// Annex B: a CallExpression as LHS is always a runtime ReferenceError.
// Evaluate the call (side effects), then throw — never evaluate the RHS.
#[cfg(feature = "annex-b")]
if let AssignTarget::Call(call) = assign.lhs() {
compiler.compile_call_as_invalid_lhs(call, dst);
return;
}

if assign.op() == AssignOp::Assign {
match Access::from_assign_target(assign.lhs()) {
Ok(access) => {
Expand Down
11 changes: 10 additions & 1 deletion core/engine/src/bytecompiler/expression/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ use crate::bytecompiler::{
use boa_ast::{
expression::{
access::{PropertyAccess, PropertyAccessField},
operator::{Update, update::UpdateOp},
operator::{Update, update::{UpdateOp, UpdateTarget}},
},
scope::BindingLocatorError,
};

impl ByteCompiler<'_> {
pub(crate) fn compile_update(&mut self, update: &Update, dst: &Register, discard: bool) {
let mut compiler = self.position_guard(update);

// Annex B: a CallExpression as update operand is always a runtime ReferenceError.
// Evaluate the call (side effects), then throw — never perform the increment/decrement.
#[cfg(feature = "annex-b")]
if let UpdateTarget::Call(call) = update.target() {
compiler.compile_call_as_invalid_lhs(call, dst);
return;
}

let increment = matches!(
update.op(),
UpdateOp::IncrementPost | UpdateOp::IncrementPre
Expand Down
27 changes: 27 additions & 0 deletions core/engine/src/bytecompiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ impl Access<'_> {
AssignTarget::Identifier(ident) => Ok(Access::Variable { name: *ident }),
AssignTarget::Access(access) => Ok(Access::Property { access }),
AssignTarget::Pattern(pat) => Err(pat),
// Annex B: handled before this function is called (compile_call_as_invalid_lhs).
#[cfg(feature = "annex-b")]
AssignTarget::Call(_) => unreachable!(),
}
}

Expand All @@ -433,6 +436,9 @@ impl Access<'_> {
match target {
UpdateTarget::Identifier(name) => Access::Variable { name: *name },
UpdateTarget::PropertyAccess(access) => Access::Property { access },
// Annex B: handled before this function is called (compile_call_as_invalid_lhs).
#[cfg(feature = "annex-b")]
UpdateTarget::Call(_) => unreachable!(),
}
}
}
Expand Down Expand Up @@ -1708,6 +1714,27 @@ impl<'ctx> ByteCompiler<'ctx> {
self.compile_expr_impl(expr, dst);
}

/// Annex B: compile a [`Call`] that appears as an invalid assignment target.
///
/// Evaluates the call expression for its side effects (so `f()` runs),
/// then unconditionally throws a `ReferenceError` at runtime — without
/// evaluating the RHS or performing the assignment/update.
///
/// See: <https://tc39.es/ecma262/#sec-runtime-errors-for-function-call-assignment-targets>
#[cfg(feature = "annex-b")]
pub(crate) fn compile_call_as_invalid_lhs(
&mut self,
call: &Call,
dst: &Register,
) {
self.call(Callable::Call(call), CallResultDest::Register(dst));
let error_msg = self.get_or_insert_literal(Literal::String(js_string!(
"Invalid left-hand side in assignment"
)));
self.bytecode
.emit_throw_new_reference_error(error_msg.into());
}

/// Compile an expression purely for its side effects, discarding the result.
///
/// This avoids allocating a register and emitting a store for the result
Expand Down
10 changes: 10 additions & 0 deletions core/engine/src/bytecompiler/statement/loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ impl ByteCompiler<'_> {
IterableLoopInitializer::Pattern(pattern) => {
self.compile_declaration_pattern(pattern, BindingOpcode::SetName, &value);
}
// Annex B: evaluate the call for side effects, then throw ReferenceError.
#[cfg(feature = "annex-b")]
IterableLoopInitializer::Call(call) => {
self.compile_call_as_invalid_lhs(call, &value);
}
}

self.register_allocator.dealloc(value);
Expand Down Expand Up @@ -384,6 +389,11 @@ impl ByteCompiler<'_> {
IterableLoopInitializer::Pattern(pattern) => {
self.compile_declaration_pattern(pattern, BindingOpcode::SetName, &value);
}
// Annex B: evaluate the call for side effects, then throw ReferenceError.
#[cfg(feature = "annex-b")]
IterableLoopInitializer::Call(call) => {
self.compile_call_as_invalid_lhs(call, &value);
}
}

self.register_allocator.dealloc(value);
Expand Down
17 changes: 11 additions & 6 deletions core/engine/src/tests/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ fn assignment_to_non_assignable() {

#[test]
fn assignment_to_non_assignable_ctd() {
// Annex B: CallExpression as LHS in non-strict mode is a runtime ReferenceError,
// not a parse-time SyntaxError.
// https://tc39.es/ecma262/#sec-runtime-errors-for-function-call-assignment-targets
run_test_actions(
[
"(()=>{})() -= 5",
Expand All @@ -436,8 +439,8 @@ fn assignment_to_non_assignable_ctd() {
.map(|src| {
TestAction::assert_native_error(
src,
JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 12",
JsNativeErrorKind::Reference,
"Invalid left-hand side in assignment",
)
}),
);
Expand All @@ -458,14 +461,15 @@ fn multicharacter_assignment_to_non_assignable() {

#[test]
fn multicharacter_assignment_to_non_assignable_ctd() {
// Annex B: runtime ReferenceError, not parse-time SyntaxError.
run_test_actions(
["(()=>{})() **= 5", "(()=>{})() <<= 5", "(()=>{})() >>= 5"]
.into_iter()
.map(|src| {
TestAction::assert_native_error(
src,
JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 12",
JsNativeErrorKind::Reference,
"Invalid left-hand side in assignment",
)
}),
);
Expand All @@ -488,6 +492,7 @@ fn multicharacter_bitwise_assignment_to_non_assignable() {

#[test]
fn multicharacter_bitwise_assignment_to_non_assignable_ctd() {
// Annex B: runtime ReferenceError, not parse-time SyntaxError.
run_test_actions(
[
"(()=>{})() >>>= 5",
Expand All @@ -499,8 +504,8 @@ fn multicharacter_bitwise_assignment_to_non_assignable_ctd() {
.map(|src| {
TestAction::assert_native_error(
src,
JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 12",
JsNativeErrorKind::Reference,
"Invalid left-hand side in assignment",
)
}),
);
Expand Down
Loading