From d28633925cead9e5d9f03b453ebada47d8b0c678 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:26:30 +0000 Subject: [PATCH] Implement `for await...of` loops (#2286) This Pull Request changes the following: - Implement `for await...of` loop parsing - Implement `for await...of` execution --- boa_engine/src/builtins/iterable/mod.rs | 5 +++ boa_engine/src/bytecompiler/mod.rs | 16 ++++++- .../ast/node/iteration/for_of_loop/mod.rs | 9 +++- .../statement/iteration/for_statement.rs | 34 ++++++++++++--- boa_engine/src/vm/code_block.rs | 2 + boa_engine/src/vm/mod.rs | 42 ++++++++++++++++++- boa_engine/src/vm/opcode.rs | 18 ++++++++ boa_engine/src/vm/tests.rs | 4 +- 8 files changed, 119 insertions(+), 11 deletions(-) diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index e65f02c9be..27d4d28018 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -233,6 +233,11 @@ pub struct IteratorResult { } impl IteratorResult { + /// Create a new `IteratorResult`. + pub(crate) fn new(object: JsObject) -> Self { + Self { object } + } + /// `IteratorComplete ( iterResult )` /// /// The abstract operation `IteratorComplete` takes argument `iterResult` (an `Object`) and diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 3cad0ab5fb..b176bd7dbc 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -1578,7 +1578,11 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::PopEnvironment); } - self.emit_opcode(Opcode::InitIterator); + if for_of_loop.r#await() { + self.emit_opcode(Opcode::InitIteratorAsync); + } else { + self.emit_opcode(Opcode::InitIterator); + } self.emit_opcode(Opcode::LoopStart); let start_address = self.next_opcode_location(); @@ -1588,7 +1592,15 @@ impl<'b> ByteCompiler<'b> { self.context.push_compile_time_environment(false); let push_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); - let exit = self.emit_opcode_with_operand(Opcode::ForInLoopNext); + + let exit = if for_of_loop.r#await() { + self.emit_opcode(Opcode::ForAwaitOfLoopIterate); + self.emit_opcode(Opcode::Await); + self.emit_opcode(Opcode::GeneratorNext); + self.emit_opcode_with_operand(Opcode::ForAwaitOfLoopNext) + } else { + self.emit_opcode_with_operand(Opcode::ForInLoopNext) + }; match for_of_loop.init() { IterableLoopInitializer::Identifier(ref ident) => { diff --git a/boa_engine/src/syntax/ast/node/iteration/for_of_loop/mod.rs b/boa_engine/src/syntax/ast/node/iteration/for_of_loop/mod.rs index 4bd890b09a..18df63fe5d 100644 --- a/boa_engine/src/syntax/ast/node/iteration/for_of_loop/mod.rs +++ b/boa_engine/src/syntax/ast/node/iteration/for_of_loop/mod.rs @@ -11,11 +11,12 @@ pub struct ForOfLoop { iterable: Box, body: Box, label: Option, + r#await: bool, } impl ForOfLoop { /// Creates a new "for of" loop AST node. - pub fn new(init: IterableLoopInitializer, iterable: I, body: B) -> Self + pub fn new(init: IterableLoopInitializer, iterable: I, body: B, r#await: bool) -> Self where I: Into, B: Into, @@ -25,6 +26,7 @@ impl ForOfLoop { iterable: Box::new(iterable.into()), body: Box::new(body.into()), label: None, + r#await, } } @@ -48,6 +50,11 @@ impl ForOfLoop { self.label = Some(label); } + /// Returns true if this "for...of" loop is an "for await...of" loop. + pub(crate) fn r#await(&self) -> bool { + self.r#await + } + /// Converts the "for of" loop to a string with the given indentation. pub(in crate::syntax::ast::node) fn to_indented_string( &self, diff --git a/boa_engine/src/syntax/parser/statement/iteration/for_statement.rs b/boa_engine/src/syntax/parser/statement/iteration/for_statement.rs index 6b702274da..de53269d33 100644 --- a/boa_engine/src/syntax/parser/statement/iteration/for_statement.rs +++ b/boa_engine/src/syntax/parser/statement/iteration/for_statement.rs @@ -79,10 +79,34 @@ where ) -> Result { let _timer = Profiler::global().start_event("ForStatement", "Parsing"); cursor.expect((Keyword::For, false), "for statement", interner)?; - let init_position = cursor - .expect(Punctuator::OpenParen, "for statement", interner)? - .span() - .end(); + + let mut r#await = false; + + let next = cursor.next(interner)?.ok_or(ParseError::AbruptEnd)?; + let init_position = match next.kind() { + TokenKind::Punctuator(Punctuator::OpenParen) => next.span().end(), + TokenKind::Keyword((Keyword::Await, _)) if !self.allow_await.0 => { + return Err(ParseError::unexpected( + next.to_string(interner), + next.span(), + "for await...of is only valid in async functions or async generators", + )); + } + TokenKind::Keyword((Keyword::Await, _)) => { + r#await = true; + cursor + .expect(Punctuator::OpenParen, "for await...of", interner)? + .span() + .end() + } + _ => { + return Err(ParseError::unexpected( + next.to_string(interner), + next.span(), + "for statement", + )); + } + }; let init = match cursor .peek(0, interner)? @@ -233,7 +257,7 @@ where } } - return Ok(ForOfLoop::new(init, iterable, body).into()); + return Ok(ForOfLoop::new(init, iterable, body, r#await).into()); } (Some(Node::ConstDeclList(list)), _) => { // Reject const declarations without initializers inside for loops diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index 208ee747ea..b57c6005be 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -204,6 +204,7 @@ impl CodeBlock { | Opcode::SuperCall | Opcode::ForInLoopInitIterator | Opcode::ForInLoopNext + | Opcode::ForAwaitOfLoopNext | Opcode::ConcatToString | Opcode::GeneratorNextDelegate => { let result = self.read::(*pc).to_string(); @@ -374,6 +375,7 @@ impl CodeBlock { | Opcode::CallSpread | Opcode::NewSpread | Opcode::SuperCallSpread + | Opcode::ForAwaitOfLoopIterate | Opcode::Nop => String::new(), } } diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 2b036924f5..161ec8439a 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -6,7 +6,7 @@ use crate::{ builtins::{ async_generator::{AsyncGenerator, AsyncGeneratorState}, function::{ConstructorKind, Function}, - iterable::{IteratorHint, IteratorRecord}, + iterable::{IteratorHint, IteratorRecord, IteratorResult}, Array, ForInIterator, JsArgs, Number, Promise, }, environments::EnvironmentSlots, @@ -2129,6 +2129,46 @@ impl Context { self.vm.push(done); } } + Opcode::ForAwaitOfLoopIterate => { + let _done = self + .vm + .pop() + .as_boolean() + .expect("iterator [[Done]] was not a boolean"); + let next_method = self.vm.pop(); + let next_method_object = if let Some(object) = next_method.as_callable() { + object + } else { + return self.throw_type_error("iterable next method not a function"); + }; + let iterator = self.vm.pop(); + let next_result = next_method_object.call(&iterator, &[], self)?; + self.vm.push(iterator); + self.vm.push(next_method); + self.vm.push(next_result); + } + Opcode::ForAwaitOfLoopNext => { + let address = self.vm.read::(); + + let next_result = self.vm.pop(); + let next_result = if let Some(next_result) = next_result.as_object() { + IteratorResult::new(next_result.clone()) + } else { + return self.throw_type_error("next value should be an object"); + }; + + if next_result.complete(self)? { + self.vm.frame_mut().pc = address as usize; + self.vm.frame_mut().loop_env_stack_dec(); + self.vm.frame_mut().try_env_stack_dec(); + self.realm.environments.pop(); + self.vm.push(true); + } else { + self.vm.push(false); + let value = next_result.value(self)?; + self.vm.push(value); + } + } Opcode::ConcatToString => { let value_count = self.vm.read::(); let mut strings = Vec::with_capacity(value_count as usize); diff --git a/boa_engine/src/vm/opcode.rs b/boa_engine/src/vm/opcode.rs index 8d83986d54..e70767dc5d 100644 --- a/boa_engine/src/vm/opcode.rs +++ b/boa_engine/src/vm/opcode.rs @@ -1085,6 +1085,20 @@ pub enum Opcode { /// Stack: iterator, next_method, done **=>** iterator, next_method, done, next_result ForInLoopNext, + /// Move to the next value in a for await..of loop. + /// + /// Operands: + /// + /// Stack: iterator, next_method, done **=>** iterator, next_method, next_result + ForAwaitOfLoopIterate, + + /// Get the value from a for await..of loop next result. + /// + /// Operands: address: `u32` + /// + /// Stack: next_result **=>** done, value + ForAwaitOfLoopNext, + /// Concat multiple stack objects into a string. /// /// Operands: value_count: `u32` @@ -1337,6 +1351,8 @@ impl Opcode { Self::IteratorClose => "IteratorClose", Self::IteratorToArray => "IteratorToArray", Self::ForInLoopNext => "ForInLoopNext", + Self::ForAwaitOfLoopNext => "ForAwaitOfLoopNext", + Self::ForAwaitOfLoopIterate => "ForAwaitOfLoopIterate", Self::ConcatToString => "ConcatToString", Self::RequireObjectCoercible => "RequireObjectCoercible", Self::ValueNotNullOrUndefined => "ValueNotNullOrUndefined", @@ -1481,6 +1497,8 @@ impl Opcode { Self::IteratorClose => "INST - IteratorClose", Self::IteratorToArray => "INST - IteratorToArray", Self::ForInLoopNext => "INST - ForInLoopNext", + Self::ForAwaitOfLoopIterate => "INST - ForAwaitOfLoopIterate", + Self::ForAwaitOfLoopNext => "INST - ForAwaitOfLoopNext", Self::ConcatToString => "INST - ConcatToString", Self::RequireObjectCoercible => "INST - RequireObjectCoercible", Self::ValueNotNullOrUndefined => "INST - ValueNotNullOrUndefined", diff --git a/boa_engine/src/vm/tests.rs b/boa_engine/src/vm/tests.rs index cd1b1108c0..636e616a09 100644 --- a/boa_engine/src/vm/tests.rs +++ b/boa_engine/src/vm/tests.rs @@ -154,7 +154,7 @@ fn run_super_method_in_object() { assert_eq!( Context::default().eval(source.as_bytes()), Ok(JsValue::from("super")) - ) + ); } #[test] @@ -180,5 +180,5 @@ fn get_reference_by_super() { assert_eq!( Context::default().eval(source.as_bytes()), Ok(JsValue::from("ab")) - ) + ); }