Browse Source

Implement var initializers in for-in loops (#2842)

This Pull Request implements [Initializers in ForIn Statement Heads](https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads) from the Annex B. This also cleans up the "annex-b" feature to be able to disable it with `--no-default-features`, since I couldn't test the error messages when the feature is disabled.
pull/2743/head
José Julián Espina 2 years ago
parent
commit
53e4825a19
  1. 18
      boa_ast/src/statement/iteration/mod.rs
  2. 5
      boa_cli/Cargo.toml
  3. 40
      boa_engine/src/bytecompiler/statement/loop.rs
  4. 14
      boa_parser/src/error.rs
  5. 2
      boa_parser/src/lexer/mod.rs
  6. 11
      boa_parser/src/parser/statement/if_stm/mod.rs
  7. 48
      boa_parser/src/parser/statement/iteration/for_statement.rs
  8. 18
      boa_parser/src/parser/statement/labelled_stm/mod.rs
  9. 9
      boa_parser/src/parser/statement/variable/mod.rs
  10. 5
      boa_tester/Cargo.toml
  11. 5
      boa_wasm/Cargo.toml

18
boa_ast/src/statement/iteration/mod.rs

@ -9,7 +9,7 @@ mod for_of_loop;
mod while_loop; mod while_loop;
use crate::{ use crate::{
declaration::Binding, declaration::{Binding, Variable},
expression::{access::PropertyAccess, Identifier}, expression::{access::PropertyAccess, Identifier},
pattern::Pattern, pattern::Pattern,
}; };
@ -43,7 +43,7 @@ pub enum IterableLoopInitializer {
/// A property access. /// A property access.
Access(PropertyAccess), Access(PropertyAccess),
/// A new var declaration. /// A new var declaration.
Var(Binding), Var(Variable),
/// A new let declaration. /// A new let declaration.
Let(Binding), Let(Binding),
/// A new const declaration. /// A new const declaration.
@ -58,12 +58,12 @@ impl ToInternedString for IterableLoopInitializer {
Self::Identifier(ident) => return ident.to_interned_string(interner), Self::Identifier(ident) => return ident.to_interned_string(interner),
Self::Pattern(pattern) => return pattern.to_interned_string(interner), Self::Pattern(pattern) => return pattern.to_interned_string(interner),
Self::Access(access) => return access.to_interned_string(interner), Self::Access(access) => return access.to_interned_string(interner),
Self::Var(binding) => (binding, "var"), Self::Var(binding) => (binding.to_interned_string(interner), "var"),
Self::Let(binding) => (binding, "let"), Self::Let(binding) => (binding.to_interned_string(interner), "let"),
Self::Const(binding) => (binding, "const"), Self::Const(binding) => (binding.to_interned_string(interner), "const"),
}; };
format!("{pre} {}", binding.to_interned_string(interner)) format!("{pre} {binding}")
} }
} }
@ -75,7 +75,8 @@ impl VisitWith for IterableLoopInitializer {
match self { match self {
Self::Identifier(id) => visitor.visit_identifier(id), Self::Identifier(id) => visitor.visit_identifier(id),
Self::Access(pa) => visitor.visit_property_access(pa), Self::Access(pa) => visitor.visit_property_access(pa),
Self::Var(b) | Self::Let(b) | Self::Const(b) => visitor.visit_binding(b), Self::Var(b) => visitor.visit_variable(b),
Self::Let(b) | Self::Const(b) => visitor.visit_binding(b),
Self::Pattern(p) => visitor.visit_pattern(p), Self::Pattern(p) => visitor.visit_pattern(p),
} }
} }
@ -87,7 +88,8 @@ impl VisitWith for IterableLoopInitializer {
match self { match self {
Self::Identifier(id) => visitor.visit_identifier_mut(id), Self::Identifier(id) => visitor.visit_identifier_mut(id),
Self::Access(pa) => visitor.visit_property_access_mut(pa), Self::Access(pa) => visitor.visit_property_access_mut(pa),
Self::Var(b) | Self::Let(b) | Self::Const(b) => visitor.visit_binding_mut(b), 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), Self::Pattern(p) => visitor.visit_pattern_mut(p),
} }
} }

5
boa_cli/Cargo.toml

@ -12,7 +12,7 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
boa_engine = { workspace = true, features = ["deser", "console", "flowgraph", "trace", "annex-b"] } boa_engine = { workspace = true, features = ["deser", "flowgraph", "trace", "console"] }
boa_ast = { workspace = true, features = ["serde"] } boa_ast = { workspace = true, features = ["serde"] }
boa_parser.workspace = true boa_parser.workspace = true
boa_gc.workspace = true boa_gc.workspace = true
@ -26,8 +26,7 @@ phf = { version = "0.11.1", features = ["macros"] }
pollster = "0.3.0" pollster = "0.3.0"
[features] [features]
default = ["intl"] default = ["boa_engine/annex-b", "boa_engine/intl"]
intl = ["boa_engine/intl"]
[target.x86_64-unknown-linux-gnu.dependencies] [target.x86_64-unknown-linux-gnu.dependencies]
jemallocator = "0.5.0" jemallocator = "0.5.0"

40
boa_engine/src/bytecompiler/statement/loop.rs

@ -89,6 +89,16 @@ impl ByteCompiler<'_, '_> {
label: Option<Sym>, label: Option<Sym>,
configurable_globals: bool, configurable_globals: bool,
) { ) {
// Handle https://tc39.es/ecma262/#prod-annexB-ForInOfStatement
if let IterableLoopInitializer::Var(var) = for_in_loop.initializer() {
if let Binding::Identifier(ident) = var.binding() {
if let Some(init) = var.init() {
self.compile_expr(init, true);
self.create_mutable_binding(*ident, true, true);
self.emit_binding(BindingOpcode::InitVar, *ident);
}
}
}
let initializer_bound_names = match for_in_loop.initializer() { let initializer_bound_names = match for_in_loop.initializer() {
IterableLoopInitializer::Let(declaration) IterableLoopInitializer::Let(declaration)
| IterableLoopInitializer::Const(declaration) => bound_names(declaration), | IterableLoopInitializer::Const(declaration) => bound_names(declaration),
@ -137,9 +147,7 @@ impl ByteCompiler<'_, '_> {
match for_in_loop.initializer() { match for_in_loop.initializer() {
IterableLoopInitializer::Identifier(ident) => { IterableLoopInitializer::Identifier(ident) => {
self.create_mutable_binding(*ident, true, true); self.create_mutable_binding(*ident, true, true);
let binding = self.set_mutable_binding(*ident); self.emit_binding(BindingOpcode::InitVar, *ident);
let index = self.get_or_insert_binding(binding);
self.emit(Opcode::DefInitVar, &[index]);
} }
IterableLoopInitializer::Access(access) => { IterableLoopInitializer::Access(access) => {
self.access_set( self.access_set(
@ -148,7 +156,7 @@ impl ByteCompiler<'_, '_> {
ByteCompiler::access_set_top_of_stack_expr_fn, ByteCompiler::access_set_top_of_stack_expr_fn,
); );
} }
IterableLoopInitializer::Var(declaration) => match declaration { IterableLoopInitializer::Var(declaration) => match declaration.binding() {
Binding::Identifier(ident) => { Binding::Identifier(ident) => {
self.create_mutable_binding(*ident, true, configurable_globals); self.create_mutable_binding(*ident, true, configurable_globals);
self.emit_binding(BindingOpcode::InitVar, *ident); self.emit_binding(BindingOpcode::InitVar, *ident);
@ -284,18 +292,22 @@ impl ByteCompiler<'_, '_> {
ByteCompiler::access_set_top_of_stack_expr_fn, ByteCompiler::access_set_top_of_stack_expr_fn,
); );
} }
IterableLoopInitializer::Var(declaration) => match declaration { IterableLoopInitializer::Var(declaration) => {
Binding::Identifier(ident) => { // ignore initializers since those aren't allowed on for-of loops.
self.create_mutable_binding(*ident, true, false); assert!(declaration.init().is_none());
self.emit_binding(BindingOpcode::InitVar, *ident); match declaration.binding() {
} Binding::Identifier(ident) => {
Binding::Pattern(pattern) => { self.create_mutable_binding(*ident, true, false);
for ident in bound_names(pattern) { self.emit_binding(BindingOpcode::InitVar, *ident);
self.create_mutable_binding(ident, true, false); }
Binding::Pattern(pattern) => {
for ident in bound_names(pattern) {
self.create_mutable_binding(ident, true, false);
}
self.compile_declaration_pattern(pattern, BindingOpcode::InitVar);
} }
self.compile_declaration_pattern(pattern, BindingOpcode::InitVar);
} }
}, }
IterableLoopInitializer::Let(declaration) => match declaration { IterableLoopInitializer::Let(declaration) => match declaration {
Binding::Identifier(ident) => { Binding::Identifier(ident) => {
self.create_mutable_binding(*ident, false, false); self.create_mutable_binding(*ident, false, false);

14
boa_parser/src/error.rs

@ -125,18 +125,22 @@ impl Error {
} }
} }
/// Creates a "general" parsing error with the specific error message for a wrong function declaration in non-strict mode. /// Creates a "general" parsing error with the specific error message for a misplaced function declaration.
pub(crate) fn wrong_function_declaration_non_strict(position: Position) -> Self { pub(crate) fn misplaced_function_declaration(position: Position, strict: bool) -> Self {
Self::General { Self::General {
message: "In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.".into(), message: format!(
position "{}functions can only be declared at the top level or inside a block.",
if strict { "in strict mode code, " } else { "" }
)
.into(),
position,
} }
} }
/// Creates a "general" parsing error with the specific error message for a wrong function declaration with label. /// Creates a "general" parsing error with the specific error message for a wrong function declaration with label.
pub(crate) fn wrong_labelled_function_declaration(position: Position) -> Self { pub(crate) fn wrong_labelled_function_declaration(position: Position) -> Self {
Self::General { Self::General {
message: "Labelled functions can only be declared at top level or inside a block" message: "labelled functions can only be declared at the top level or inside a block"
.into(), .into(),
position, position,
} }

2
boa_parser/src/lexer/mod.rs

@ -178,7 +178,7 @@ impl<R> Lexer<R> {
where where
R: Read, R: Read,
{ {
if !cfg!(feature = "annex-b") || self.module() { if cfg!(not(feature = "annex-b")) || self.module() {
return Ok(()); return Ok(());
} }

11
boa_parser/src/parser/statement/if_stm/mod.rs

@ -77,9 +77,8 @@ where
TokenKind::Keyword((Keyword::Function, _)) => { TokenKind::Keyword((Keyword::Function, _)) => {
// FunctionDeclarations in IfStatement Statement Clauses // FunctionDeclarations in IfStatement Statement Clauses
// https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses // https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses
if strict { if cfg!(not(feature = "annex-b")) || strict {
// This production only applies when parsing non-strict code. return Err(Error::misplaced_function_declaration(position, strict));
return Err(Error::wrong_function_declaration_non_strict(position));
} }
// Source text matched by this production is processed as if each matching // Source text matched by this production is processed as if each matching
// occurrence of FunctionDeclaration[?Yield, ?Await, ~Default] was the sole // occurrence of FunctionDeclaration[?Yield, ?Await, ~Default] was the sole
@ -117,8 +116,10 @@ where
TokenKind::Keyword((Keyword::Function, _)) => { TokenKind::Keyword((Keyword::Function, _)) => {
// FunctionDeclarations in IfStatement Statement Clauses // FunctionDeclarations in IfStatement Statement Clauses
// https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses // https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses
if strict { if cfg!(not(feature = "annex-b")) || strict {
return Err(Error::wrong_function_declaration_non_strict(position)); return Err(Error::misplaced_function_declaration(
position, strict,
));
} }
// Source text matched by this production is processed as if each matching // Source text matched by this production is processed as if each matching

48
boa_parser/src/parser/statement/iteration/for_statement.rs

@ -17,7 +17,10 @@ use crate::{
}, },
Error, Error,
}; };
use ast::operations::{bound_names, var_declared_names}; use ast::{
declaration::Binding,
operations::{bound_names, var_declared_names},
};
use boa_ast::{ use boa_ast::{
self as ast, self as ast,
statement::{ statement::{
@ -157,9 +160,13 @@ where
)); ));
} }
(Some(init), TokenKind::Keyword((kw @ (Keyword::In | Keyword::Of), false))) => { (Some(init), TokenKind::Keyword((kw @ (Keyword::In | Keyword::Of), false))) => {
let kw = *kw; let in_loop = kw == &Keyword::In;
let init = let init = initializer_to_iterable_loop_initializer(
initializer_to_iterable_loop_initializer(init, position, cursor.strict())?; init,
position,
cursor.strict(),
in_loop,
)?;
cursor.advance(interner); cursor.advance(interner);
let expr = Expression::new(None, true, self.allow_yield, self.allow_await) let expr = Expression::new(None, true, self.allow_yield, self.allow_await)
@ -208,7 +215,7 @@ where
} }
} }
} }
return Ok(if kw == Keyword::In { return Ok(if in_loop {
ForInLoop::new(init, expr, body).into() ForInLoop::new(init, expr, body).into()
} else { } else {
ForOfLoop::new(init, expr, body, r#await).into() ForOfLoop::new(init, expr, body, r#await).into()
@ -288,7 +295,9 @@ fn initializer_to_iterable_loop_initializer(
initializer: ForLoopInitializer, initializer: ForLoopInitializer,
position: Position, position: Position,
strict: bool, strict: bool,
in_loop: bool,
) -> ParseResult<IterableLoopInitializer> { ) -> ParseResult<IterableLoopInitializer> {
let loop_type = if in_loop { "for-in" } else { "for-of" };
match initializer { match initializer {
ForLoopInitializer::Expression(mut expr) => { ForLoopInitializer::Expression(mut expr) => {
while let ast::Expression::Parenthesized(p) = expr { while let ast::Expression::Parenthesized(p) = expr {
@ -338,7 +347,7 @@ fn initializer_to_iterable_loop_initializer(
[declaration] => { [declaration] => {
if declaration.init().is_some() { if declaration.init().is_some() {
return Err(Error::lex(LexError::Syntax( return Err(Error::lex(LexError::Syntax(
"a declaration in the head of a for-of loop can't have an initializer" format!("a lexical declaration in the head of a {loop_type} loop can't have an initializer")
.into(), .into(),
position, position,
))); )));
@ -353,25 +362,38 @@ fn initializer_to_iterable_loop_initializer(
}) })
} }
_ => Err(Error::lex(LexError::Syntax( _ => Err(Error::lex(LexError::Syntax(
"only one variable can be declared in the head of a for-of loop".into(), format!("only one variable can be declared in the head of a {loop_type} loop")
.into(),
position, position,
))), ))),
}, },
ForLoopInitializer::Var(decl) => match decl.0.as_ref() { ForLoopInitializer::Var(decl) => match decl.0.as_ref() {
[declaration] => { [declaration] => {
// TODO: implement initializers in ForIn heads
// https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads // https://tc39.es/ecma262/#sec-initializers-in-forin-statement-heads
if declaration.init().is_some() { let is_pattern = matches!(declaration.binding(), Binding::Pattern(_));
if declaration.init().is_some()
&& (cfg!(not(feature = "annex-b")) || strict || !in_loop || is_pattern)
{
return Err(Error::lex(LexError::Syntax( return Err(Error::lex(LexError::Syntax(
"a declaration in the head of a for-of loop can't have an initializer" format!(
.into(), "{}a {} declaration in the head of a {loop_type} loop \
cannot have an initializer",
if strict { "in strict mode, " } else { "" },
if is_pattern {
"binding pattern"
} else {
"binding declaration"
}
)
.into(),
position, position,
))); )));
} }
Ok(IterableLoopInitializer::Var(declaration.binding().clone())) Ok(IterableLoopInitializer::Var(declaration.clone()))
} }
_ => Err(Error::lex(LexError::Syntax( _ => Err(Error::lex(LexError::Syntax(
"only one variable can be declared in the head of a for-of loop".into(), format!("only one variable can be declared in the head of a {loop_type} loop")
.into(),
position, position,
))), ))),
}, },

18
boa_parser/src/parser/statement/labelled_stm/mod.rs

@ -65,18 +65,22 @@ where
// Early Error: It is a Syntax Error if any strict mode source code matches this rule. // Early Error: It is a Syntax Error if any strict mode source code matches this rule.
// https://tc39.es/ecma262/#sec-labelled-statements-static-semantics-early-errors // https://tc39.es/ecma262/#sec-labelled-statements-static-semantics-early-errors
// https://tc39.es/ecma262/#sec-labelled-function-declarations // https://tc39.es/ecma262/#sec-labelled-function-declarations
TokenKind::Keyword((Keyword::Function, _)) if strict => { TokenKind::Keyword((Keyword::Function, _))
return Err(Error::general( if cfg!(not(feature = "annex-b")) || strict =>
"In strict mode code, functions can only be declared at top level or inside a block.", {
next_token.span().start() return Err(Error::misplaced_function_declaration(
next_token.span().start(),
strict,
)) ))
} }
TokenKind::Keyword((Keyword::Function, _)) => { TokenKind::Keyword((Keyword::Function, _)) => {
FunctionDeclaration::new(self.allow_yield, self.allow_await, false) FunctionDeclaration::new(self.allow_yield, self.allow_await, false)
.parse(cursor, interner)? .parse(cursor, interner)?
.into() .into()
} }
_ => Statement::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor, interner)?.into() _ => Statement::new(self.allow_yield, self.allow_await, self.allow_return)
.parse(cursor, interner)?
.into(),
}; };
Ok(ast::statement::Labelled::new(labelled_item, label)) Ok(ast::statement::Labelled::new(labelled_item, label))

9
boa_parser/src/parser/statement/variable/mod.rs

@ -214,8 +214,13 @@ where
.is_some() .is_some()
{ {
Some( Some(
Initializer::new(Some(ident), true, self.allow_yield, self.allow_await) Initializer::new(
.parse(cursor, interner)?, Some(ident),
self.allow_in,
self.allow_yield,
self.allow_await,
)
.parse(cursor, interner)?,
) )
} else { } else {
None None

5
boa_tester/Cargo.toml

@ -12,7 +12,7 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
boa_engine = { workspace = true, features = ["annex-b"] } boa_engine.workspace = true
boa_gc.workspace = true boa_gc.workspace = true
clap = { version = "4.2.4", features = ["derive"] } clap = { version = "4.2.4", features = ["derive"] }
serde = { version = "1.0.160", features = ["derive"] } serde = { version = "1.0.160", features = ["derive"] }
@ -31,5 +31,4 @@ comfy-table = "6.1.4"
serde_repr = "0.1.12" serde_repr = "0.1.12"
[features] [features]
default = ["intl"] default = ["boa_engine/intl", "boa_engine/annex-b"]
intl = ["boa_engine/intl"]

5
boa_wasm/Cargo.toml

@ -12,11 +12,14 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
boa_engine = { workspace = true, features = ["console", "annex-b"] } boa_engine = { workspace = true, features = ["console"] }
wasm-bindgen = "0.2.84" wasm-bindgen = "0.2.84"
getrandom = { version = "0.2.9", features = ["js"] } getrandom = { version = "0.2.9", features = ["js"] }
chrono = { version = "0.4.24", features = ["clock", "std", "wasmbind"] } chrono = { version = "0.4.24", features = ["clock", "std", "wasmbind"] }
[features]
default = ["boa_engine/annex-b"]
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]
name = "boa_wasm" name = "boa_wasm"

Loading…
Cancel
Save