diff --git a/core/ast/src/expression/literal/array.rs b/core/ast/src/expression/literal/array.rs index 9172f771773..7949cb5a5e2 100644 --- a/core/ast/src/expression/literal/array.rs +++ b/core/ast/src/expression/literal/array.rs @@ -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) => { diff --git a/core/ast/src/expression/literal/object.rs b/core/ast/src/expression/literal/object.rs index 9d135e467ed..c43aa76c0b2 100644 --- a/core/ast/src/expression/literal/object.rs +++ b/core/ast/src/expression/literal/object.rs @@ -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)) => { diff --git a/core/ast/src/expression/operator/assign/mod.rs b/core/ast/src/expression/operator/assign/mod.rs index 17dd47fa108..b8a9c4d8caa 100644 --- a/core/ast/src/expression/operator/assign/mod.rs +++ b/core/ast/src/expression/operator/assign/mod.rs @@ -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}, }; @@ -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), } impl AssignTarget { @@ -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, } } @@ -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(), } } } @@ -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), } } } @@ -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), } } @@ -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), } } } diff --git a/core/ast/src/expression/operator/update/mod.rs b/core/ast/src/expression/operator/update/mod.rs index 87043b70a35..951b232bdfb 100644 --- a/core/ast/src/expression/operator/update/mod.rs +++ b/core/ast/src/expression/operator/update/mod.rs @@ -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}; @@ -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), } } @@ -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), } } } @@ -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), } impl ToInternedString for UpdateTarget { @@ -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(), } } } diff --git a/core/ast/src/statement/iteration/mod.rs b/core/ast/src/statement/iteration/mod.rs index 9bad14f4bb8..332b09e406a 100644 --- a/core/ast/src/statement/iteration/mod.rs +++ b/core/ast/src/statement/iteration/mod.rs @@ -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; @@ -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), } impl ToInternedString for IterableLoopInitializer { @@ -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"), @@ -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), } } @@ -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), } } } diff --git a/core/engine/src/bytecompiler/expression/assign.rs b/core/engine/src/bytecompiler/expression/assign.rs index 8613087eeb9..ffa5aadd091 100644 --- a/core/engine/src/bytecompiler/expression/assign.rs +++ b/core/engine/src/bytecompiler/expression/assign.rs @@ -6,7 +6,7 @@ use boa_ast::{ Expression, expression::{ access::{PropertyAccess, PropertyAccessField}, - operator::{Assign, assign::AssignOp}, + operator::{Assign, assign::{AssignOp, AssignTarget}}, }, scope::BindingLocatorError, }; @@ -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) => { diff --git a/core/engine/src/bytecompiler/expression/update.rs b/core/engine/src/bytecompiler/expression/update.rs index 7caf49fcb62..93c0796f08a 100644 --- a/core/engine/src/bytecompiler/expression/update.rs +++ b/core/engine/src/bytecompiler/expression/update.rs @@ -4,7 +4,7 @@ use crate::bytecompiler::{ use boa_ast::{ expression::{ access::{PropertyAccess, PropertyAccessField}, - operator::{Update, update::UpdateOp}, + operator::{Update, update::{UpdateOp, UpdateTarget}}, }, scope::BindingLocatorError, }; @@ -12,6 +12,15 @@ use boa_ast::{ 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 diff --git a/core/engine/src/bytecompiler/mod.rs b/core/engine/src/bytecompiler/mod.rs index b093f3c0008..e391a439d05 100644 --- a/core/engine/src/bytecompiler/mod.rs +++ b/core/engine/src/bytecompiler/mod.rs @@ -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!(), } } @@ -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!(), } } } @@ -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: + #[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 diff --git a/core/engine/src/bytecompiler/statement/loop.rs b/core/engine/src/bytecompiler/statement/loop.rs index 0c046ae3f37..3a021204bf8 100644 --- a/core/engine/src/bytecompiler/statement/loop.rs +++ b/core/engine/src/bytecompiler/statement/loop.rs @@ -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); @@ -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); diff --git a/core/engine/src/tests/operators.rs b/core/engine/src/tests/operators.rs index 728131b841f..19a86216d9b 100644 --- a/core/engine/src/tests/operators.rs +++ b/core/engine/src/tests/operators.rs @@ -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", @@ -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", ) }), ); @@ -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", ) }), ); @@ -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", @@ -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", ) }), ); diff --git a/core/parser/src/parser/expression/primary/mod.rs b/core/parser/src/parser/expression/primary/mod.rs index 6b532da224c..2292a59939d 100644 --- a/core/parser/src/parser/expression/primary/mod.rs +++ b/core/parser/src/parser/expression/primary/mod.rs @@ -609,6 +609,14 @@ fn expression_to_formal_parameters( span.start(), )); } + // Annex B: a Call as assignment target is not valid in a formal parameter list. + #[cfg(feature = "annex-b")] + AssignTarget::Call(_) => { + return Err(Error::general( + "invalid initialization expression in formal parameter list", + span.start(), + )); + } }, ast::Expression::ObjectLiteral(object) => { let pattern = object.to_pattern(strict).ok_or_else(|| { diff --git a/core/parser/src/parser/expression/update.rs b/core/parser/src/parser/expression/update.rs index b5ecd28949d..777f87bc9c6 100644 --- a/core/parser/src/parser/expression/update.rs +++ b/core/parser/src/parser/expression/update.rs @@ -74,6 +74,12 @@ fn as_simple( Ok(Some(UpdateTarget::PropertyAccess(access.clone()))) } Expression::Parenthesized(p) => as_simple(p.expression(), position, strict), + // Annex B: in non-strict mode a CallExpression is a valid (runtime-error) update target. + // The ReferenceError is emitted at runtime by the bytecompiler. + #[cfg(feature = "annex-b")] + Expression::Call(call) if !strict => { + Ok(Some(UpdateTarget::Call(Box::new(call.clone())))) + } _ => Ok(None), } } diff --git a/core/parser/src/parser/statement/iteration/for_statement.rs b/core/parser/src/parser/statement/iteration/for_statement.rs index e64d48ab6b9..3460edf271e 100644 --- a/core/parser/src/parser/statement/iteration/for_statement.rs +++ b/core/parser/src/parser/statement/iteration/for_statement.rs @@ -370,6 +370,12 @@ fn initializer_to_iterable_loop_initializer( ast::Expression::PropertyAccess(access) => { Ok(IterableLoopInitializer::Access(access)) } + // Annex B: in non-strict mode a CallExpression is a valid (runtime-error) + // for-in/of LHS. The ReferenceError is emitted at runtime by the bytecompiler. + #[cfg(feature = "annex-b")] + ast::Expression::Call(call) if !strict => { + Ok(IterableLoopInitializer::Call(Box::new(call))) + } _ => Err(Error::lex(LexError::Syntax( "invalid variable for iterable loop".into(), position,