From a989462e25e95c533cbc2a6df39474ed0e3b7876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Tue, 6 Jun 2023 19:17:49 +0000 Subject: [PATCH] Unify async iterators and iterators compilation (#2976) * Close active iterators when returning from async generator * Fix remaining async generator tests * Fix remaining async generator tests * Replace some reserved opcodes * Fix for await of loop flag * Add tests for fix * Remove whitespace * Fix test display and improve test * Change `usize` to `u32` --- .vscode/launch.json | 4 +- .vscode/tasks.json | 10 + boa_cli/src/main.rs | 4 +- boa_engine/src/builtins/array/mod.rs | 19 +- .../src/builtins/async_generator/mod.rs | 67 +--- .../src/builtins/intl/list_format/mod.rs | 6 +- .../iterable/async_from_sync_iterator.rs | 76 +--- boa_engine/src/builtins/iterable/mod.rs | 167 +++++--- boa_engine/src/builtins/map/mod.rs | 8 +- boa_engine/src/builtins/promise/mod.rs | 260 +++++------- boa_engine/src/builtins/set/mod.rs | 8 +- boa_engine/src/builtins/weak_set/mod.rs | 8 +- .../declaration/declaration_pattern.rs | 85 +--- boa_engine/src/bytecompiler/declarations.rs | 10 +- boa_engine/src/bytecompiler/expression/mod.rs | 74 ++-- boa_engine/src/bytecompiler/jump_control.rs | 39 +- boa_engine/src/bytecompiler/mod.rs | 26 +- .../src/bytecompiler/statement/continue.rs | 25 +- boa_engine/src/bytecompiler/statement/loop.rs | 37 +- boa_engine/src/bytecompiler/statement/mod.rs | 4 + boa_engine/src/bytecompiler/utils.rs | 93 +++-- boa_engine/src/job.rs | 4 - boa_engine/src/tests/iterators.rs | 222 ++++++++++ boa_engine/src/tests/mod.rs | 1 + boa_engine/src/vm/call_frame/env_stack.rs | 27 +- boa_engine/src/vm/call_frame/mod.rs | 9 +- boa_engine/src/vm/code_block.rs | 55 +-- boa_engine/src/vm/completion_record.rs | 16 +- boa_engine/src/vm/flowgraph/graph.rs | 45 ++- boa_engine/src/vm/flowgraph/mod.rs | 143 ++++--- boa_engine/src/vm/mod.rs | 6 +- .../src/vm/opcode/control_flow/throw.rs | 87 ++-- boa_engine/src/vm/opcode/generator/mod.rs | 378 +++++------------- .../src/vm/opcode/generator/yield_stm.rs | 86 +++- boa_engine/src/vm/opcode/iteration/for_in.rs | 10 +- boa_engine/src/vm/opcode/iteration/get.rs | 6 +- .../src/vm/opcode/iteration/iterator.rs | 336 +++++++++------- .../src/vm/opcode/iteration/loop_ops.rs | 23 ++ boa_engine/src/vm/opcode/mod.rs | 151 ++++--- boa_engine/src/vm/opcode/push/array.rs | 18 +- 40 files changed, 1460 insertions(+), 1193 deletions(-) create mode 100644 boa_engine/src/tests/iterators.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index b0de59888a..55b75dc5ab 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "program": "${workspaceFolder}/target/debug/boa", "args": ["${workspaceFolder}/tests/js/test.js", "--debug-object"], "sourceLanguages": ["rust"], - "preLaunchTask": "Cargo Build" + "preLaunchTask": "Cargo Build boa_cli" }, { "type": "lldb", @@ -32,7 +32,7 @@ "tests/js" ], "sourceLanguages": ["rust"], - "preLaunchTask": "Cargo Build" + "preLaunchTask": "Cargo Build boa_cli" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ca67f62f35..1c0b67ad97 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,6 +13,16 @@ "clear": true } }, + { + "type": "process", + "label": "Cargo Build boa_cli", + "command": "cargo", + "args": ["build", "-p", "boa_cli"], + "group": "build", + "presentation": { + "clear": true + } + }, { "type": "process", "label": "Cargo Run", diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index fc0e45db1d..0e9ace99d5 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -487,7 +487,7 @@ struct Jobs(RefCell>); impl JobQueue for Jobs { fn enqueue_promise_job(&self, job: NativeJob, _: &mut Context<'_>) { - self.0.borrow_mut().push_front(job); + self.0.borrow_mut().push_back(job); } fn run_jobs(&self, context: &mut Context<'_>) { @@ -506,6 +506,6 @@ impl JobQueue for Jobs { fn enqueue_future_job(&self, future: FutureJob, _: &mut Context<'_>) { let job = pollster::block_on(future); - self.0.borrow_mut().push_front(job); + self.0.borrow_mut().push_back(job); } } diff --git a/boa_engine/src/builtins/array/mod.rs b/boa_engine/src/builtins/array/mod.rs index 8f684027bb..245b30a28e 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -561,7 +561,7 @@ impl Array { }; // c. Let iteratorRecord be ? GetIterator(items, sync, usingIterator). - let iterator_record = + let mut iterator_record = items.get_iterator(context, Some(IteratorHint::Sync), Some(using_iterator))?; // d. Let k be 0. @@ -571,19 +571,16 @@ impl Array { // x. Set k to k + 1. for k in 0..9_007_199_254_740_991_u64 { // iii. Let next be ? IteratorStep(iteratorRecord). - let next = iterator_record.step(context)?; + if iterator_record.step(context)? { + // 1. Perform ? Set(A, "length", 𝔽(k), true). + a.set(utf16!("length"), k, true, context)?; + // 2. Return A. + return Ok(a.into()); + } // iv. If next is false, then - let Some(next) = next else { - // 1. Perform ? Set(A, "length", 𝔽(k), true). - a.set(utf16!("length"), k, true, context)?; - - // 2. Return A. - return Ok(a.into()); - }; - // v. Let nextValue be ? IteratorValue(next). - let next_value = next.value(context)?; + let next_value = iterator_record.value(context)?; // vi. If mapping is true, then let mapped_value = if let Some(mapfn) = mapping { diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index 872767db95..22cc8f532a 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -18,7 +18,7 @@ use crate::{ realm::Realm, symbol::JsSymbol, value::JsValue, - vm::GeneratorResumeKind, + vm::{CompletionRecord, GeneratorResumeKind}, Context, JsArgs, JsError, JsResult, }; use boa_gc::{Finalize, Trace}; @@ -46,7 +46,7 @@ pub(crate) enum AsyncGeneratorState { #[derive(Debug, Clone, Finalize, Trace)] pub(crate) struct AsyncGeneratorRequest { /// The `[[Completion]]` slot. - pub(crate) completion: (JsResult, bool), + pub(crate) completion: CompletionRecord, /// The `[[Capability]]` slot. capability: PromiseCapability, @@ -164,7 +164,7 @@ impl AsyncGenerator { } // 7. Let completion be NormalCompletion(value). - let completion = (Ok(args.get_or_undefined(0).clone()), false); + let completion = CompletionRecord::Normal(args.get_or_undefined(0).clone()); // 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). generator.enqueue(completion.clone(), promise_capability.clone()); @@ -232,7 +232,8 @@ impl AsyncGenerator { if_abrupt_reject_promise!(generator, promise_capability, context); // 5. Let completion be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. - let completion = (Ok(args.get_or_undefined(0).clone()), true); + let return_value = args.get_or_undefined(0).clone(); + let completion = CompletionRecord::Return(return_value.clone()); // 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). generator.enqueue(completion.clone(), promise_capability.clone()); @@ -246,14 +247,8 @@ impl AsyncGenerator { generator.state = AsyncGeneratorState::AwaitingReturn; // b. Perform ! AsyncGeneratorAwaitReturn(generator). - let next = generator - .queue - .front() - .cloned() - .expect("queue cannot be empty here"); drop(generator_obj_mut); - let (completion, _) = &next.completion; - Self::await_return(generator_object.clone(), completion.clone(), context); + Self::await_return(generator_object.clone(), return_value, context); } // 9. Else if state is suspendedYield, then else if state == AsyncGeneratorState::SuspendedYield { @@ -346,10 +341,8 @@ impl AsyncGenerator { } // 8. Let completion be ThrowCompletion(exception). - let completion = ( - Err(JsError::from_opaque(args.get_or_undefined(0).clone())), - false, - ); + let completion = + CompletionRecord::Throw(JsError::from_opaque(args.get_or_undefined(0).clone())); // 9. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). generator.enqueue(completion.clone(), promise_capability.clone()); @@ -384,7 +377,7 @@ impl AsyncGenerator { /// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorenqueue pub(crate) fn enqueue( &mut self, - completion: (JsResult, bool), + completion: CompletionRecord, promise_capability: PromiseCapability, ) { // 1. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }. @@ -469,7 +462,7 @@ impl AsyncGenerator { generator: &JsObject, state: AsyncGeneratorState, mut generator_context: GeneratorContext, - completion: (JsResult, bool), + completion: CompletionRecord, context: &mut Context<'_>, ) { // 1. Assert: generator.[[AsyncGeneratorState]] is either suspendedStart or suspendedYield. @@ -491,15 +484,9 @@ impl AsyncGenerator { .state = AsyncGeneratorState::Executing; let (value, resume_kind) = match completion { - (Ok(value), r#return) => ( - value, - if r#return { - GeneratorResumeKind::Return - } else { - GeneratorResumeKind::Normal - }, - ), - (Err(value), _) => (value.to_opaque(context), GeneratorResumeKind::Throw), + CompletionRecord::Normal(val) => (val, GeneratorResumeKind::Normal), + CompletionRecord::Return(val) => (val, GeneratorResumeKind::Return), + CompletionRecord::Throw(err) => (err.to_opaque(context), GeneratorResumeKind::Throw), }; // 6. Push genContext onto the execution context stack; genContext is now the running execution context. @@ -527,19 +514,12 @@ impl AsyncGenerator { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn - pub(crate) fn await_return( - generator: JsObject, - completion: JsResult, - context: &mut Context<'_>, - ) { + pub(crate) fn await_return(generator: JsObject, value: JsValue, context: &mut Context<'_>) { // 1. Let queue be generator.[[AsyncGeneratorQueue]]. // 2. Assert: queue is not empty. // 3. Let next be the first element of queue. // 4. Let completion be Completion(next.[[Completion]]). - // 5. Assert: completion.[[Type]] is return. - let value = completion.expect("completion must be a return completion"); - // Note: The spec is currently broken here. // See: https://github.com/tc39/ecma262/pull/2683 @@ -681,29 +661,24 @@ impl AsyncGenerator { let next = queue.front().expect("must have entry"); // b. Let completion be Completion(next.[[Completion]]). - match &next.completion { + match next.completion.clone() { // c. If completion.[[Type]] is return, then - (completion, true) => { + CompletionRecord::Return(val) => { // i. Set generator.[[AsyncGeneratorState]] to awaiting-return. gen.state = AsyncGeneratorState::AwaitingReturn; + drop(generator_borrow_mut); // ii. Perform ! AsyncGeneratorAwaitReturn(generator). - let completion = completion.clone(); - drop(generator_borrow_mut); - Self::await_return(generator.clone(), completion, context); + Self::await_return(generator.clone(), val, context); // iii. Set done to true. break; } // d. Else, - (completion, false) => { + completion => { // i. If completion.[[Type]] is normal, then - let completion = if completion.is_ok() { - // 1. Set completion to NormalCompletion(undefined). - Ok(JsValue::undefined()) - } else { - completion.clone() - }; + // 1. Set completion to NormalCompletion(undefined). + let completion = completion.consume().map(|_| JsValue::undefined()); // ii. Perform AsyncGeneratorCompleteStep(generator, completion, true). let next = queue.pop_front().expect("must have entry"); diff --git a/boa_engine/src/builtins/intl/list_format/mod.rs b/boa_engine/src/builtins/intl/list_format/mod.rs index e17054e57d..63c92682a7 100644 --- a/boa_engine/src/builtins/intl/list_format/mod.rs +++ b/boa_engine/src/builtins/intl/list_format/mod.rs @@ -469,7 +469,7 @@ fn string_list_from_iterable( } // 2. Let iteratorRecord be ? GetIterator(iterable). - let iterator = iterable.get_iterator(context, None, None)?; + let mut iterator = iterable.get_iterator(context, None, None)?; // 3. Let list be a new empty List. let mut list = Vec::new(); @@ -478,9 +478,9 @@ fn string_list_from_iterable( // 5. Repeat, while next is not false, // a. Set next to ? IteratorStep(iteratorRecord). // b. If next is not false, then - while let Some(item) = iterator.step(context)? { + while !iterator.step(context)? { + let item = iterator.value(context)?; // i. Let nextValue be ? IteratorValue(next). - let item = item.value(context)?; // ii. If Type(nextValue) is not String, then let Some(s) = item.as_string().cloned() else { // 1. Let error be ThrowCompletion(a newly created TypeError object). diff --git a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs index 5557b7427d..45098226af 100644 --- a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs +++ b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs @@ -9,7 +9,7 @@ use crate::{ object::{FunctionObjectBuilder, JsObject, ObjectData}, realm::Realm, string::utf16, - Context, JsArgs, JsNativeError, JsResult, JsValue, + Context, JsArgs, JsResult, JsValue, }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; @@ -84,7 +84,7 @@ impl AsyncFromSyncIterator { // 4. Let iteratorRecord be the Iterator Record { [[Iterator]]: asyncIterator, [[NextMethod]]: nextMethod, [[Done]]: false }. // 5. Return iteratorRecord. - IteratorRecord::new(async_iterator, next_method, false) + IteratorRecord::new(async_iterator, next_method) } /// `%AsyncFromSyncIteratorPrototype%.next ( [ value ] )` @@ -117,7 +117,15 @@ impl AsyncFromSyncIterator { // a. Let result be Completion(IteratorNext(syncIteratorRecord, value)). // 6. Else, // a. Let result be Completion(IteratorNext(syncIteratorRecord)). - let result = sync_iterator_record.next(args.get(0), context); + let iterator = sync_iterator_record.iterator().clone(); + let next = sync_iterator_record.next_method(); + let result = next + .call( + &iterator.into(), + args.get(0).map_or(&[], std::slice::from_ref), + context, + ) + .and_then(IteratorResult::from_value); // 7. IfAbruptRejectPromise(result, promiseCapability). if_abrupt_reject_promise!(result, promise_capability, context); @@ -187,36 +195,15 @@ impl AsyncFromSyncIterator { } }; + // 11. If Type(result) is not Object, then + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + let result = result.and_then(IteratorResult::from_value); + // 10. IfAbruptRejectPromise(result, promiseCapability). if_abrupt_reject_promise!(result, promise_capability, context); - let Some(result) = result.as_object() else { - // 11. If Type(result) is not Object, then - // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). - promise_capability - .reject() - .call( - &JsValue::Undefined, - &[JsNativeError::typ() - .with_message("iterator return function returned non-object") - .to_opaque(context) - .into()], - context, - ) - .expect("cannot fail according to spec"); - - // b. Return promiseCapability.[[Promise]]. - return Ok(promise_capability.promise().clone().into()); - }; - // 12. Return AsyncFromSyncIteratorContinuation(result, promiseCapability). - Self::continuation( - &IteratorResult { - object: result.clone(), - }, - &promise_capability, - context, - ) + Self::continuation(&result, &promise_capability, context) } /// `%AsyncFromSyncIteratorPrototype%.throw ( [ value ] )` @@ -280,36 +267,15 @@ impl AsyncFromSyncIterator { } }; + // 11. If Type(result) is not Object, then + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + let result = result.and_then(IteratorResult::from_value); + // 10. IfAbruptRejectPromise(result, promiseCapability). if_abrupt_reject_promise!(result, promise_capability, context); - let Some(result) = result.as_object() else { - // 11. If Type(result) is not Object, then - // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). - promise_capability - .reject() - .call( - &JsValue::Undefined, - &[JsNativeError::typ() - .with_message("iterator throw function returned non-object") - .to_opaque(context) - .into()], - context, - ) - .expect("cannot fail according to spec"); - - // b. Return promiseCapability.[[Promise]]. - return Ok(promise_capability.promise().clone().into()); - }; - // 12. Return AsyncFromSyncIteratorContinuation(result, promiseCapability). - Self::continuation( - &IteratorResult { - object: result.clone(), - }, - &promise_capability, - context, - ) + Self::continuation(&result, &promise_capability, context) } /// `AsyncFromSyncIteratorContinuation ( result, promiseCapability )` diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index e6dddab4da..13a3126025 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -285,24 +285,32 @@ impl JsValue { // 6. Let iteratorRecord be the Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }. // 7. Return iteratorRecord. - Ok(IteratorRecord::new( - iterator_obj.clone(), - next_method, - false, - )) + Ok(IteratorRecord::new(iterator_obj.clone(), next_method)) } } /// The result of the iteration process. -#[derive(Debug)] +#[derive(Debug, Clone, Trace, Finalize)] pub struct IteratorResult { object: JsObject, } impl IteratorResult { - /// Create a new `IteratorResult`. - pub(crate) fn new(object: JsObject) -> Self { - Self { object } + /// Gets a new `IteratorResult` from a value. Returns `Err` if + /// the value is not a [`JsObject`] + pub(crate) fn from_value(value: JsValue) -> JsResult { + if let JsValue::Object(o) = value { + Ok(Self { object: o }) + } else { + Err(JsNativeError::typ() + .with_message("next value should be an object") + .into()) + } + } + + /// Gets the inner object of this `IteratorResult`. + pub(crate) const fn object(&self) -> &JsObject { + &self.object } /// `IteratorComplete ( iterResult )` @@ -362,16 +370,22 @@ pub struct IteratorRecord { /// /// Whether the iterator has been closed. done: bool, + + /// The result of the last call to `next`. + last_result: IteratorResult, } impl IteratorRecord { /// Creates a new `IteratorRecord` with the given iterator object, next method and `done` flag. #[inline] - pub fn new(iterator: JsObject, next_method: JsValue, done: bool) -> Self { + pub fn new(iterator: JsObject, next_method: JsValue) -> Self { Self { iterator, next_method, - done, + done: false, + last_result: IteratorResult { + object: JsObject::with_null_proto(), + }, } } @@ -380,19 +394,63 @@ impl IteratorRecord { &self.iterator } - /// Get the `[[NextMethod]]` field of the `IteratorRecord`. + /// Gets the `[[NextMethod]]` field of the `IteratorRecord`. pub(crate) const fn next_method(&self) -> &JsValue { &self.next_method } + /// Gets the last result object of the iterator record. + pub(crate) const fn last_result(&self) -> &IteratorResult { + &self.last_result + } + + /// Runs `f`, setting the `done` field of this `IteratorRecord` to `true` if `f` returns + /// an error. + fn set_done_on_err(&mut self, f: F) -> JsResult + where + F: FnOnce(&mut Self) -> JsResult, + { + let result = f(self); + if result.is_err() { + self.done = true; + } + result + } + + /// Gets the current value of the `IteratorRecord`. + pub(crate) fn value(&mut self, context: &mut Context<'_>) -> JsResult { + self.set_done_on_err(|iter| iter.last_result.value(context)) + } + /// Get the `[[Done]]` field of the `IteratorRecord`. pub(crate) const fn done(&self) -> bool { self.done } - /// Sets the `[[Done]]` field of the `IteratorRecord`. - pub(crate) fn set_done(&mut self, done: bool) { - self.done = done; + /// Updates the current result value of this iterator record. + pub(crate) fn update_result( + &mut self, + result: JsValue, + context: &mut Context<'_>, + ) -> JsResult<()> { + self.set_done_on_err(|iter| { + // 3. If Type(result) is not Object, throw a TypeError exception. + // 4. Return result. + // `IteratorResult::from_value` does this for us. + + // `IteratorStep(iteratorRecord)` + // https://tc39.es/ecma262/#sec-iteratorstep + + // 1. Let result be ? IteratorNext(iteratorRecord). + let result = IteratorResult::from_value(result)?; + // 2. Let done be ? IteratorComplete(result). + // 3. If done is true, return false. + iter.done = result.complete(context)?; + + iter.last_result = result; + + Ok(()) + }) } /// `IteratorNext ( iteratorRecord [ , value ] )` @@ -405,63 +463,43 @@ impl IteratorRecord { /// - [ECMA reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-iteratornext - pub(crate) fn next( - &self, + pub(crate) fn step_with( + &mut self, value: Option<&JsValue>, context: &mut Context<'_>, - ) -> JsResult { - let _timer = Profiler::global().start_event("IteratorRecord::next", "iterator"); - - // 1. If value is not present, then - // a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]). - // 2. Else, - // a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]], « value »). - let result = self.next_method.call( - &self.iterator.clone().into(), - value.map_or(&[], std::slice::from_ref), - context, - )?; - - // 3. If Type(result) is not Object, throw a TypeError exception. - // 4. Return result. - result - .as_object() - .map(|o| IteratorResult { object: o.clone() }) - .ok_or_else(|| { - JsNativeError::typ() - .with_message("next value should be an object") - .into() - }) + ) -> JsResult { + let _timer = Profiler::global().start_event("IteratorRecord::step_with", "iterator"); + + self.set_done_on_err(|iter| { + // 1. If value is not present, then + // a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]). + // 2. Else, + // a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]], « value »). + let result = iter.next_method.call( + &iter.iterator.clone().into(), + value.map_or(&[], std::slice::from_ref), + context, + )?; + + iter.update_result(result, context)?; + + // 4. Return result. + Ok(iter.done) + }) } /// `IteratorStep ( iteratorRecord )` /// - /// The abstract operation `IteratorStep` takes argument `iteratorRecord` (an `Iterator` - /// Record) and returns either a normal completion containing either an `Object` or `false`, or - /// a throw completion. It requests the next value from `iteratorRecord.[[Iterator]]` by - /// calling `iteratorRecord.[[NextMethod]]` and returns either `false` indicating that the - /// iterator has reached its end or the `IteratorResult` object if a next value is available. + /// Updates the `IteratorRecord` and returns `true` if the next result record returned + /// `done: true`, otherwise returns `false`. This differs slightly from the spec, but also + /// simplifies some logic around iterators. /// /// More information: /// - [ECMA reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-iteratorstep - pub(crate) fn step(&self, context: &mut Context<'_>) -> JsResult> { - let _timer = Profiler::global().start_event("IteratorRecord::step", "iterator"); - - // 1. Let result be ? IteratorNext(iteratorRecord). - let result = self.next(None, context)?; - - // 2. Let done be ? IteratorComplete(result). - let done = result.complete(context)?; - - // 3. If done is true, return false. - if done { - return Ok(None); - } - - // 4. Return result. - Ok(Some(result)) + pub(crate) fn step(&mut self, context: &mut Context<'_>) -> JsResult { + self.step_with(None, context) } /// `IteratorClose ( iteratorRecord, completion )` @@ -548,7 +586,7 @@ pub(crate) fn iterable_to_list( // a. Let iteratorRecord be ? GetIterator(items, sync, method). // 2. Else, // a. Let iteratorRecord be ? GetIterator(items, sync). - let iterator_record = items.get_iterator(context, Some(IteratorHint::Sync), method)?; + let mut iterator_record = items.get_iterator(context, Some(IteratorHint::Sync), method)?; // 3. Let values be a new empty List. let mut values = Vec::new(); @@ -559,9 +597,8 @@ pub(crate) fn iterable_to_list( // b. If next is not false, then // i. Let nextValue be ? IteratorValue(next). // ii. Append nextValue to the end of the List values. - while let Some(next) = iterator_record.step(context)? { - let next_value = next.value(context)?; - values.push(next_value); + while !iterator_record.step(context)? { + values.push(iterator_record.value(context)?); } // 6. Return values. diff --git a/boa_engine/src/builtins/map/mod.rs b/boa_engine/src/builtins/map/mod.rs index f8d04e4fc1..bafcc2c98e 100644 --- a/boa_engine/src/builtins/map/mod.rs +++ b/boa_engine/src/builtins/map/mod.rs @@ -542,20 +542,18 @@ pub(crate) fn add_entries_from_iterable( })?; // 2. Let iteratorRecord be ? GetIterator(iterable). - let iterator_record = iterable.get_iterator(context, None, None)?; + let mut iterator_record = iterable.get_iterator(context, None, None)?; // 3. Repeat, loop { // a. Let next be ? IteratorStep(iteratorRecord). - let next = iterator_record.step(context)?; - // b. If next is false, return target. // c. Let nextItem be ? IteratorValue(next). - let Some(next_item) = next else { + if iterator_record.step(context)? { return Ok(target.clone().into()); }; - let next_item = next_item.value(context)?; + let next_item = iterator_record.value(context)?; let Some(next_item) = next_item.as_object() else { // d. If Type(nextItem) is not Object, then diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 7ced9a8848..136f1346a3 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -542,59 +542,40 @@ impl Promise { // 4. Repeat, loop { // a. Let next be Completion(IteratorStep(iteratorRecord)). - let next = iterator_record.step(context); - - let next_value = match next { - Err(e) => { - // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - - // c. ReturnIfAbrupt(next). - return Err(e); - } - // d. If next is false, then - Ok(None) => { - // i. Set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - - // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. - remaining_elements_count.set(remaining_elements_count.get() - 1); - - // iii. If remainingElementsCount.[[Value]] is 0, then - if remaining_elements_count.get() == 0 { - // 1. Let valuesArray be CreateArrayFromList(values). - let values_array = crate::builtins::Array::create_array_from_list( - values.borrow().iter().cloned(), - context, - ); - - // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). - result_capability.resolve.call( - &JsValue::undefined(), - &[values_array.into()], - context, - )?; - } + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + // c. ReturnIfAbrupt(next). + let done = iterator_record.step(context)?; + + // d. If next is false, then + // i. Set iteratorRecord.[[Done]] to true. + if done { + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + values.borrow().iter().cloned(), + context, + ); - // iv. Return resultCapability.[[Promise]]. - return Ok(result_capability.promise.clone()); + // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). + result_capability.resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + )?; } - Ok(Some(next)) => { - // e. Let nextValue be Completion(IteratorValue(next)). - let next_value = next.value(context); - match next_value { - Err(e) => { - // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } - // g. ReturnIfAbrupt(nextValue). - return Err(e); - } - Ok(next_value) => next_value, - } - } - }; + // e. Let nextValue be Completion(IteratorValue(next)). + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + // g. ReturnIfAbrupt(nextValue). + let next_value = iterator_record.value(context)?; // h. Append undefined to values. values.borrow_mut().push(JsValue::Undefined); @@ -785,59 +766,40 @@ impl Promise { // 4. Repeat, loop { // a. Let next be Completion(IteratorStep(iteratorRecord)). - let next = iterator_record.step(context); - - let next_value = match next { - Err(e) => { - // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - - // c. ReturnIfAbrupt(next). - return Err(e); - } - // d. If next is false, then - Ok(None) => { - // i. Set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - - // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. - remaining_elements_count.set(remaining_elements_count.get() - 1); - - // iii. If remainingElementsCount.[[Value]] is 0, then - if remaining_elements_count.get() == 0 { - // 1. Let valuesArray be CreateArrayFromList(values). - let values_array = crate::builtins::Array::create_array_from_list( - values.borrow().as_slice().iter().cloned(), - context, - ); + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + // c. ReturnIfAbrupt(next). + let done = iterator_record.step(context)?; - // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). - result_capability.resolve.call( - &JsValue::undefined(), - &[values_array.into()], - context, - )?; - } + // d. If next is false, then + if done { + // i. Set iteratorRecord.[[Done]] to true. + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + values.borrow().as_slice().iter().cloned(), + context, + ); - // iv. Return resultCapability.[[Promise]]. - return Ok(result_capability.promise.clone()); + // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). + result_capability.resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + )?; } - Ok(Some(next)) => { - // e. Let nextValue be Completion(IteratorValue(next)). - let next_value = next.value(context); - match next_value { - Err(e) => { - // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } - // g. ReturnIfAbrupt(nextValue). - return Err(e); - } - Ok(next_value) => next_value, - } - } - }; + // e. Let nextValue be Completion(IteratorValue(next)). + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + // g. ReturnIfAbrupt(nextValue). + let next_value = iterator_record.value(context)?; // h. Append undefined to values. values.borrow_mut().push(JsValue::undefined()); @@ -1131,61 +1093,43 @@ impl Promise { // 4. Repeat, loop { // a. Let next be Completion(IteratorStep(iteratorRecord)). - let next = iterator_record.step(context); - - let next_value = match next { - Err(e) => { - // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - - // c. ReturnIfAbrupt(next). - return Err(e); - } - // d. If next is false, then - Ok(None) => { - // i. Set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - - // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. - remaining_elements_count.set(remaining_elements_count.get() - 1); - - // iii. If remainingElementsCount.[[Value]] is 0, then - if remaining_elements_count.get() == 0 { - // 1. Let error be a newly created AggregateError object. - // 2. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }). - let error = JsNativeError::aggregate( - errors - .borrow() - .iter() - .cloned() - .map(JsError::from_opaque) - .collect(), - ) - .with_message("no promise in Promise.any was fulfilled."); + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + // c. ReturnIfAbrupt(next). + let done = iterator_record.step(context)?; - // 3. Return ThrowCompletion(error). - return Err(error.into()); - } + // d. If next is false, then + if done { + // i. Set iteratorRecord.[[Done]] to true. - // iv. Return resultCapability.[[Promise]]. - return Ok(result_capability.promise.clone()); + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let error be a newly created AggregateError object. + // 2. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }). + let error = JsNativeError::aggregate( + errors + .borrow() + .iter() + .cloned() + .map(JsError::from_opaque) + .collect(), + ) + .with_message("no promise in Promise.any was fulfilled."); + + // 3. Return ThrowCompletion(error). + return Err(error.into()); } - Ok(Some(next)) => { - // e. Let nextValue be Completion(IteratorValue(next)). - let next_value = next.value(context); - match next_value { - Err(e) => { - // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } - // g. ReturnIfAbrupt(nextValue). - return Err(e); - } - Ok(next_value) => next_value, - } - } - }; + // e. Let nextValue be Completion(IteratorValue(next)). + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + // g. ReturnIfAbrupt(nextValue). + let next_value = iterator_record.value(context)?; // h. Append undefined to errors. errors.borrow_mut().push(JsValue::undefined()); @@ -1375,35 +1319,21 @@ impl Promise { // 1. Repeat, loop { // a. Let next be Completion(IteratorStep(iteratorRecord)). - let next = iterator_record.step(context); - // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. - if next.is_err() { - iterator_record.set_done(true); - } - // c. ReturnIfAbrupt(next). - let next = next?; + let done = iterator_record.step(context)?; - let Some(next) = next else { + if done { // d. If next is false, then // i. Set iteratorRecord.[[Done]] to true. - iterator_record.set_done(true); - // ii. Return resultCapability.[[Promise]]. return Ok(result_capability.promise.clone()); - }; + } // e. Let nextValue be Completion(IteratorValue(next)). - let next_value = next.value(context); - // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. - if next_value.is_err() { - iterator_record.set_done(true); - } - // g. ReturnIfAbrupt(nextValue). - let next_value = next_value?; + let next_value = iterator_record.value(context)?; // h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). let next_promise = promise_resolve.call(&constructor, &[next_value], context)?; diff --git a/boa_engine/src/builtins/set/mod.rs b/boa_engine/src/builtins/set/mod.rs index 656f27c89e..82048b149d 100644 --- a/boa_engine/src/builtins/set/mod.rs +++ b/boa_engine/src/builtins/set/mod.rs @@ -145,7 +145,7 @@ impl BuiltInConstructor for Set { })?; // 7. Let iteratorRecord be ? GetIterator(iterable). - let iterator_record = iterable.clone().get_iterator(context, None, None)?; + let mut iterator_record = iterable.clone().get_iterator(context, None, None)?; // 8. Repeat, // a. Let next be ? IteratorStep(iteratorRecord). @@ -153,12 +153,12 @@ impl BuiltInConstructor for Set { // c. Let nextValue be ? IteratorValue(next). // d. Let status be Completion(Call(adder, set, « nextValue »)). // e. IfAbruptCloseIterator(status, iteratorRecord). - while let Some(next) = iterator_record.step(context)? { + while !iterator_record.step(context)? { + let next = iterator_record.value(context)?; // c - let next_value = next.value(context)?; // d, e - if let Err(status) = adder.call(&set.clone().into(), &[next_value], context) { + if let Err(status) = adder.call(&set.clone().into(), &[next], context) { return iterator_record.close(Err(status), context); } } diff --git a/boa_engine/src/builtins/weak_set/mod.rs b/boa_engine/src/builtins/weak_set/mod.rs index 13304a39f8..75a4accab3 100644 --- a/boa_engine/src/builtins/weak_set/mod.rs +++ b/boa_engine/src/builtins/weak_set/mod.rs @@ -101,17 +101,17 @@ impl BuiltInConstructor for WeakSet { .ok_or_else(|| JsNativeError::typ().with_message("WeakSet: 'add' is not a function"))?; // 7. Let iteratorRecord be ? GetIterator(iterable). - let iterator_record = iterable.clone().get_iterator(context, None, None)?; + let mut iterator_record = iterable.clone().get_iterator(context, None, None)?; // 8. Repeat, // a. Let next be ? IteratorStep(iteratorRecord). - while let Some(next) = iterator_record.step(context)? { + while !iterator_record.step(context)? { // c. Let nextValue be ? IteratorValue(next). - let next_value = next.value(context)?; + let next = iterator_record.value(context)?; // d. Let status be Completion(Call(adder, set, « nextValue »)). // e. IfAbruptCloseIterator(status, iteratorRecord). - if let Err(status) = adder.call(&weak_set.clone().into(), &[next_value], context) { + if let Err(status) = adder.call(&weak_set.clone().into(), &[next], context) { return iterator_record.close(Err(status), context); } } diff --git a/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs b/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs index cea8c2ecd4..9ac426d060 100644 --- a/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs +++ b/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs @@ -176,54 +176,34 @@ impl ByteCompiler<'_, '_> { Pattern::Array(pattern) => { self.emit_opcode(Opcode::ValueNotNullOrUndefined); self.emit_opcode(Opcode::GetIterator); - self.emit_opcode(Opcode::IteratorClosePush); - match pattern.bindings().split_last() { - None => self.emit_opcode(Opcode::PushFalse), - Some((last, rest)) => { - for element in rest { - self.compile_array_pattern_element(element, def, false); - } - self.compile_array_pattern_element(last, def, true); - } + + for element in pattern.bindings() { + self.compile_array_pattern_element(element, def); } - self.emit_opcode(Opcode::IteratorClosePop); + self.iterator_close(false); } } } - fn compile_array_pattern_element( - &mut self, - element: &ArrayPatternElement, - def: BindingOpcode, - with_done: bool, - ) { + fn compile_array_pattern_element(&mut self, element: &ArrayPatternElement, def: BindingOpcode) { use ArrayPatternElement::{ Elision, Pattern, PatternRest, PropertyAccess, PropertyAccessRest, SingleName, SingleNameRest, }; - let unwrapping = if with_done { - Opcode::IteratorUnwrapNext - } else { - Opcode::IteratorUnwrapValue - }; match element { // ArrayBindingPattern : [ Elision ] Elision => { - self.emit_opcode(Opcode::IteratorNextSetDone); - if with_done { - self.emit_opcode(Opcode::IteratorUnwrapNext); - } - self.emit_opcode(Opcode::Pop); + self.emit_opcode(Opcode::IteratorNext); } // SingleNameBinding : BindingIdentifier Initializer[opt] SingleName { ident, default_init, } => { - self.emit_opcode(Opcode::IteratorNextSetDone); - self.emit_opcode(unwrapping); + self.emit_opcode(Opcode::IteratorNext); + self.emit_opcode(Opcode::IteratorValue); if let Some(init) = default_init { let skip = self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); self.compile_expr(init, true); @@ -232,23 +212,9 @@ impl ByteCompiler<'_, '_> { self.emit_binding(def, *ident); } PropertyAccess { access } => { - self.access_set(Access::Property { access }, false, |compiler, level| { - if level != 0 { - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 2); - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 2); - } - compiler.emit_opcode(Opcode::IteratorNextSetDone); - compiler.emit_opcode(unwrapping); - if level != 0 { - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 3 + u8::from(with_done)); - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 3 + u8::from(with_done)); - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 1); - } + self.access_set(Access::Property { access }, false, |compiler, _level| { + compiler.emit_opcode(Opcode::IteratorNext); + compiler.emit_opcode(Opcode::IteratorValue); }); } // BindingElement : BindingPattern Initializer[opt] @@ -256,8 +222,8 @@ impl ByteCompiler<'_, '_> { pattern, default_init, } => { - self.emit_opcode(Opcode::IteratorNextSetDone); - self.emit_opcode(unwrapping); + self.emit_opcode(Opcode::IteratorNext); + self.emit_opcode(Opcode::IteratorValue); if let Some(init) = default_init { let skip = self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); @@ -271,39 +237,16 @@ impl ByteCompiler<'_, '_> { SingleNameRest { ident } => { self.emit_opcode(Opcode::IteratorToArray); self.emit_binding(def, *ident); - if with_done { - self.emit_opcode(Opcode::PushTrue); - } } PropertyAccessRest { access } => { - self.access_set(Access::Property { access }, false, |compiler, level| { - if level != 0 { - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 2); - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 2); - } + self.access_set(Access::Property { access }, false, |compiler, _level| { compiler.emit_opcode(Opcode::IteratorToArray); - if level != 0 { - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 3); - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 3); - compiler.emit_opcode(Opcode::RotateLeft); - compiler.emit_u8(level + 1); - } }); - if with_done { - self.emit_opcode(Opcode::PushTrue); - } } // BindingRestElement : ... BindingPattern PatternRest { pattern } => { self.emit_opcode(Opcode::IteratorToArray); self.compile_declaration_pattern(pattern, def); - if with_done { - self.emit_opcode(Opcode::PushTrue); - } } } } diff --git a/boa_engine/src/bytecompiler/declarations.rs b/boa_engine/src/bytecompiler/declarations.rs index ae3ec1fe3c..b66b1d2f20 100644 --- a/boa_engine/src/bytecompiler/declarations.rs +++ b/boa_engine/src/bytecompiler/declarations.rs @@ -272,6 +272,9 @@ impl ByteCompiler<'_, '_> { self.context, ); + // Ensures global functions are printed when generating the global flowgraph. + self.functions.push(code.clone()); + // b. Let fo be InstantiateFunctionObject of f with arguments env and privateEnv. let function = if generator { create_generator_function_object(code, r#async, None, self.context) @@ -686,6 +689,9 @@ impl ByteCompiler<'_, '_> { // c. If varEnv is a Global Environment Record, then if var_environment_is_global { + // Ensures global functions are printed when generating the global flowgraph. + self.functions.push(code.clone()); + // b. Let fo be InstantiateFunctionObject of f with arguments lexEnv and privateEnv. let function = if generator { create_generator_function_object(code, r#async, None, self.context) @@ -988,7 +994,9 @@ impl ByteCompiler<'_, '_> { } if generator { self.emit_opcode(Opcode::PushUndefined); - self.emit_opcode(Opcode::Yield); + // Don't need to use `AsyncGeneratorYield` since + // we just want to stop the execution of the generator. + self.emit_opcode(Opcode::GeneratorYield); } // 27. If hasParameterExpressions is false, then diff --git a/boa_engine/src/bytecompiler/expression/mod.rs b/boa_engine/src/bytecompiler/expression/mod.rs index 88a9ccf74b..9ed2e4987d 100644 --- a/boa_engine/src/bytecompiler/expression/mod.rs +++ b/boa_engine/src/bytecompiler/expression/mod.rs @@ -4,7 +4,7 @@ mod object_literal; mod unary; mod update; -use super::{Access, Callable, Label, NodeKind}; +use super::{Access, Callable, NodeKind}; use crate::{ bytecompiler::{ByteCompiler, Literal}, vm::Opcode, @@ -161,64 +161,48 @@ impl ByteCompiler<'_, '_> { self.emit_opcode(Opcode::PushUndefined); } - if r#yield.delegate() && self.in_async_generator { - self.emit_opcode(Opcode::GetAsyncIterator); - self.emit_opcode(Opcode::PushUndefined); + if r#yield.delegate() { + if self.in_async_generator { + self.emit_opcode(Opcode::GetAsyncIterator); + } else { + self.emit_opcode(Opcode::GetIterator); + } + self.emit_opcode(Opcode::PushUndefined); let start_address = self.next_opcode_location(); let (throw_method_undefined, return_method_undefined) = - self.emit_opcode_with_two_operands(Opcode::GeneratorAsyncDelegateNext); - self.emit_opcode(Opcode::Await); - - let skip_yield = Label { - index: self.next_opcode_location(), - }; - let exit = Label { - index: self.next_opcode_location() + 8, - }; - self.emit( - Opcode::GeneratorAsyncDelegateResume, - &[Self::DUMMY_ADDRESS, start_address, Self::DUMMY_ADDRESS], - ); + self.emit_opcode_with_two_operands(Opcode::GeneratorDelegateNext); - self.emit_opcode(Opcode::PushUndefined); - self.emit_opcode(Opcode::Yield); + if self.in_async_generator { + self.emit_opcode(Opcode::Await); + } + + let (return_gen, exit) = + self.emit_opcode_with_two_operands(Opcode::GeneratorDelegateResume); + if self.in_async_generator { + self.emit_opcode(Opcode::IteratorValue); + self.async_generator_yield(); + } else { + self.emit_opcode(Opcode::IteratorResult); + self.emit_opcode(Opcode::GeneratorYield); + } self.emit(Opcode::Jump, &[start_address]); - self.patch_jump(skip_yield); + self.patch_jump(return_gen); self.patch_jump(return_method_undefined); - self.emit_opcode(Opcode::Await); + if self.in_async_generator { + self.emit_opcode(Opcode::Await); + } + self.close_active_iterators(); self.emit_opcode(Opcode::GeneratorResumeReturn); self.patch_jump(throw_method_undefined); - self.iterator_close(true); + self.iterator_close(self.in_async_generator); self.emit_opcode(Opcode::Throw); self.patch_jump(exit); - } else if r#yield.delegate() { - self.emit_opcode(Opcode::GetIterator); - self.emit_opcode(Opcode::PushUndefined); - let start_address = self.next_opcode_location(); - let start = self.emit_opcode_with_operand(Opcode::GeneratorNextDelegate); - self.emit(Opcode::Jump, &[start_address]); - self.patch_jump(start); - } else if self.in_async_generator { - self.emit_opcode(Opcode::Await); - let (skip_yield, skip_yield_await) = - self.emit_opcode_with_two_operands(Opcode::AsyncGeneratorNext); - self.emit_opcode(Opcode::PushUndefined); - self.emit_opcode(Opcode::Yield); - let normal_completion = - self.emit_opcode_with_operand(Opcode::GeneratorAsyncResumeYield); - self.patch_jump(skip_yield); - self.emit_opcode(Opcode::Await); - self.emit_opcode(Opcode::GeneratorResumeReturn); - - self.patch_jump(skip_yield_await); - self.patch_jump(normal_completion); } else { - self.emit_opcode(Opcode::Yield); - self.emit_opcode(Opcode::GeneratorNext); + self.r#yield(); } if !use_expr { diff --git a/boa_engine/src/bytecompiler/jump_control.rs b/boa_engine/src/bytecompiler/jump_control.rs index d93dc03b78..fe6019dfbb 100644 --- a/boa_engine/src/bytecompiler/jump_control.rs +++ b/boa_engine/src/bytecompiler/jump_control.rs @@ -27,7 +27,7 @@ pub(crate) struct JumpControlInfo { bitflags! { /// A bitflag that contains the type flags and relevant booleans for `JumpControlInfo`. #[derive(Debug, Clone, Copy)] - pub(crate) struct JumpControlInfoFlags: u8 { + pub(crate) struct JumpControlInfoFlags: u16 { const LOOP = 0b0000_0001; const SWITCH = 0b0000_0010; const TRY_BLOCK = 0b0000_0100; @@ -35,7 +35,8 @@ bitflags! { const IN_CATCH = 0b0001_0000; const IN_FINALLY = 0b0010_0000; const HAS_FINALLY = 0b0100_0000; - const FOR_OF_IN_LOOP = 0b1000_0000; + const ITERATOR_LOOP = 0b1000_0000; + const FOR_AWAIT_OF_LOOP = 0b1_0000_0000; } } @@ -95,8 +96,14 @@ impl JumpControlInfo { self } - pub(crate) fn with_for_of_in_loop(mut self, value: bool) -> Self { - self.flags.set(JumpControlInfoFlags::FOR_OF_IN_LOOP, value); + pub(crate) fn with_iterator_loop(mut self, value: bool) -> Self { + self.flags.set(JumpControlInfoFlags::ITERATOR_LOOP, value); + self + } + + pub(crate) fn with_for_await_of_loop(mut self, value: bool) -> Self { + self.flags + .set(JumpControlInfoFlags::FOR_AWAIT_OF_LOOP, value); self } } @@ -139,8 +146,12 @@ impl JumpControlInfo { self.flags.contains(JumpControlInfoFlags::HAS_FINALLY) } - pub(crate) const fn for_of_in_loop(&self) -> bool { - self.flags.contains(JumpControlInfoFlags::FOR_OF_IN_LOOP) + pub(crate) const fn iterator_loop(&self) -> bool { + self.flags.contains(JumpControlInfoFlags::ITERATOR_LOOP) + } + + pub(crate) const fn for_await_of_loop(&self) -> bool { + self.flags.contains(JumpControlInfoFlags::FOR_AWAIT_OF_LOOP) } } @@ -274,7 +285,21 @@ impl ByteCompiler<'_, '_> { .with_loop_flag(true) .with_label(label) .with_start_address(start_address) - .with_for_of_in_loop(true); + .with_iterator_loop(true); + self.jump_info.push(new_info); + } + + pub(crate) fn push_loop_control_info_for_await_of_loop( + &mut self, + label: Option, + start_address: u32, + ) { + let new_info = JumpControlInfo::default() + .with_loop_flag(true) + .with_label(label) + .with_start_address(start_address) + .with_iterator_loop(true) + .with_for_await_of_loop(true); self.jump_info.push(new_info); } diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 5fb97a2c88..b2d5a3a3c8 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -495,10 +495,6 @@ impl<'ctx, 'host> ByteCompiler<'ctx, 'host> { self.emit_opcode_with_operand(Opcode::JumpIfFalse) } - fn jump_if_not_undefined(&mut self) -> Label { - self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined) - } - fn jump_if_null_or_undefined(&mut self) -> Label { self.emit_opcode_with_operand(Opcode::JumpIfNullOrUndefined) } @@ -519,6 +515,28 @@ impl<'ctx, 'host> ByteCompiler<'ctx, 'host> { (Label { index }, Label { index: index + 4 }) } + /// Emit an opcode with three dummy operands. + /// Return the `Label`s of the three operands. + pub(crate) fn emit_opcode_with_three_operands( + &mut self, + opcode: Opcode, + ) -> (Label, Label, Label) { + let index = self.next_opcode_location(); + self.emit( + opcode, + &[ + Self::DUMMY_ADDRESS, + Self::DUMMY_ADDRESS, + Self::DUMMY_ADDRESS, + ], + ); + ( + Label { index }, + Label { index: index + 4 }, + Label { index: index + 8 }, + ) + } + pub(crate) fn patch_jump_with_target(&mut self, label: Label, target: u32) { const U32_SIZE: usize = std::mem::size_of::(); diff --git a/boa_engine/src/bytecompiler/statement/continue.rs b/boa_engine/src/bytecompiler/statement/continue.rs index 5ef8fff42d..729e9e7e35 100644 --- a/boa_engine/src/bytecompiler/statement/continue.rs +++ b/boa_engine/src/bytecompiler/statement/continue.rs @@ -18,21 +18,20 @@ impl ByteCompiler<'_, '_> { // 1. Handle if node has a label. if let Some(node_label) = node.label() { let items = self.jump_info.iter().rev().filter(|info| info.is_loop()); - let mut emit_for_of_in_exit = 0_u32; + let mut iterator_closes = Vec::new(); + for info in items { if info.label() == Some(node_label) { break; } - if info.for_of_in_loop() { - emit_for_of_in_exit += 1; + if info.iterator_loop() { + iterator_closes.push(info.for_await_of_loop()); } } - for _ in 0..emit_for_of_in_exit { - self.emit_opcode(Opcode::Pop); - self.emit_opcode(Opcode::Pop); - self.emit_opcode(Opcode::Pop); + for r#async in iterator_closes { + self.iterator_close(r#async); } let (cont_label, set_label) = self.emit_opcode_with_two_operands(Opcode::Continue); @@ -83,21 +82,19 @@ impl ByteCompiler<'_, '_> { }; } else if let Some(node_label) = node.label() { let items = self.jump_info.iter().rev().filter(|info| info.is_loop()); - let mut emit_for_of_in_exit = 0_u32; + let mut iterator_closes = Vec::new(); for info in items { if info.label() == Some(node_label) { break; } - if info.for_of_in_loop() { - emit_for_of_in_exit += 1; + if info.iterator_loop() { + iterator_closes.push(info.for_await_of_loop()); } } - for _ in 0..emit_for_of_in_exit { - self.emit_opcode(Opcode::Pop); - self.emit_opcode(Opcode::Pop); - self.emit_opcode(Opcode::Pop); + for r#async in iterator_closes { + self.iterator_close(r#async); } let (cont_label, set_label) = self.emit_opcode_with_two_operands(Opcode::Continue); diff --git a/boa_engine/src/bytecompiler/statement/loop.rs b/boa_engine/src/bytecompiler/statement/loop.rs index 298ecc689d..e5b005b0f8 100644 --- a/boa_engine/src/bytecompiler/statement/loop.rs +++ b/boa_engine/src/bytecompiler/statement/loop.rs @@ -160,17 +160,19 @@ impl ByteCompiler<'_, '_> { let early_exit = self.jump_if_null_or_undefined(); self.emit_opcode(Opcode::CreateForInIterator); - self.emit_opcode(Opcode::PushFalse); - let (loop_start, exit_label) = self.emit_opcode_with_two_operands(Opcode::LoopStart); + let (loop_start, exit_label) = + self.emit_opcode_with_two_operands(Opcode::IteratorLoopStart); let start_address = self.next_opcode_location(); self.push_loop_control_info_for_of_in_loop(label, start_address); self.emit_opcode(Opcode::LoopContinue); self.patch_jump_with_target(loop_start, start_address); - self.emit_opcode(Opcode::Pop); // pop the `done` value. self.emit_opcode(Opcode::IteratorNext); - let exit = self.emit_opcode_with_operand(Opcode::IteratorUnwrapNextOrJump); + self.emit_opcode(Opcode::IteratorDone); + let exit = self.jump_if_true(); + + self.emit_opcode(Opcode::IteratorValue); let iteration_environment = if initializer_bound_names.is_empty() { None @@ -245,11 +247,8 @@ impl ByteCompiler<'_, '_> { self.patch_jump(exit_label); self.pop_loop_control_info(); self.emit_opcode(Opcode::LoopEnd); - self.emit_opcode(Opcode::RotateRight); - self.emit_u8(4); - self.emit_opcode(Opcode::Pop); - self.emit_opcode(Opcode::Pop); - self.emit_opcode(Opcode::Pop); + + self.iterator_close(false); let skip_early_exit = self.jump(); self.patch_jump(early_exit); @@ -293,21 +292,27 @@ impl ByteCompiler<'_, '_> { } else { self.emit_opcode(Opcode::GetIterator); } - self.emit_opcode(Opcode::PushFalse); - let (loop_start, loop_exit) = self.emit_opcode_with_two_operands(Opcode::LoopStart); + let (loop_start, loop_exit) = self.emit_opcode_with_two_operands(Opcode::IteratorLoopStart); let start_address = self.next_opcode_location(); - self.push_loop_control_info_for_of_in_loop(label, start_address); + if for_of_loop.r#await() { + self.push_loop_control_info_for_await_of_loop(label, start_address); + } else { + self.push_loop_control_info_for_of_in_loop(label, start_address); + } self.emit_opcode(Opcode::LoopContinue); self.patch_jump_with_target(loop_start, start_address); - self.emit_opcode(Opcode::Pop); // pop the `done` value. self.emit_opcode(Opcode::IteratorNext); if for_of_loop.r#await() { + self.emit_opcode(Opcode::IteratorResult); self.emit_opcode(Opcode::Await); + self.emit_opcode(Opcode::IteratorFinishAsyncNext); self.emit_opcode(Opcode::GeneratorNext); } - let exit = self.emit_opcode_with_operand(Opcode::IteratorUnwrapNextOrJump); + self.emit_opcode(Opcode::IteratorDone); + let exit = self.jump_if_true(); + self.emit_opcode(Opcode::IteratorValue); let iteration_environment = if initializer_bound_names.is_empty() { None @@ -388,9 +393,9 @@ impl ByteCompiler<'_, '_> { self.patch_jump(loop_exit); self.pop_loop_control_info(); self.emit_opcode(Opcode::LoopEnd); - self.emit_opcode(Opcode::RotateRight); - self.emit_u8(4); + self.iterator_close(for_of_loop.r#await()); + if !use_expr { self.emit_opcode(Opcode::Pop); } diff --git a/boa_engine/src/bytecompiler/statement/mod.rs b/boa_engine/src/bytecompiler/statement/mod.rs index 43472fd86d..54b54c2e09 100644 --- a/boa_engine/src/bytecompiler/statement/mod.rs +++ b/boa_engine/src/bytecompiler/statement/mod.rs @@ -51,6 +51,10 @@ impl ByteCompiler<'_, '_> { Statement::Return(ret) => { if let Some(expr) = ret.target() { self.compile_expr(expr, true); + if self.in_async_generator { + self.emit_opcode(Opcode::Await); + self.emit_opcode(Opcode::GeneratorNext); + } } else { self.emit(Opcode::PushUndefined, &[]); } diff --git a/boa_engine/src/bytecompiler/utils.rs b/boa_engine/src/bytecompiler/utils.rs index 0a093836f2..e366c46880 100644 --- a/boa_engine/src/bytecompiler/utils.rs +++ b/boa_engine/src/bytecompiler/utils.rs @@ -1,5 +1,3 @@ -use boa_interner::Sym; - use crate::{js_string, vm::Opcode}; use super::{ByteCompiler, Literal}; @@ -10,36 +8,21 @@ impl ByteCompiler<'_, '_> { /// This is equivalent to the [`IteratorClose`][iter] and [`AsyncIteratorClose`][async] /// operations. /// - /// Stack: - /// - iterator, `next_method`, done **=>** \ + /// Iterator Stack: + /// - iterator **=>** \ /// /// [iter]: https://tc39.es/ecma262/#sec-iteratorclose /// [async]: https://tc39.es/ecma262/#sec-asynciteratorclose pub(super) fn iterator_close(&mut self, async_: bool) { - // Need to remove `next_method` to manipulate the iterator - self.emit_opcode(Opcode::Swap); - self.emit_opcode(Opcode::Pop); - - let skip_iter_pop = self.jump_if_false(); + self.emit_opcode(Opcode::IteratorDone); - // `iterator` is done, we can skip calling `return`. - // `iterator` is still in the stack, so pop it to cleanup. - self.emit_opcode(Opcode::Pop); - let skip_return = self.jump(); + let skip_return = self.jump_if_true(); // iterator didn't finish iterating. - self.patch_jump(skip_iter_pop); - let index = self.get_or_insert_name(Sym::RETURN.into()); - self.emit(Opcode::GetMethod, &[index]); - let skip_jump = self.jump_if_not_undefined(); + self.emit_opcode(Opcode::IteratorReturn); // `iterator` didn't have a `return` method, so we can early exit. - // `iterator` is still in the stack, so pop it to cleanup. - self.emit_opcode(Opcode::Pop); - let early_exit = self.jump(); - - self.patch_jump(skip_jump); - self.emit(Opcode::Call, &[0]); + let early_exit = self.jump_if_false(); if async_ { self.emit_opcode(Opcode::Await); self.emit_opcode(Opcode::GeneratorNext); @@ -53,7 +36,71 @@ impl ByteCompiler<'_, '_> { self.emit(Opcode::ThrowNewTypeError, &[error_msg]); self.patch_jump(skip_return); + self.emit_opcode(Opcode::IteratorPop); + self.patch_jump(skip_throw); self.patch_jump(early_exit); } + + /// Closes all active iterators in the current [`CallFrame`][crate::vm::CallFrame]. + pub(super) fn close_active_iterators(&mut self) { + let start = self.next_opcode_location(); + self.emit_opcode(Opcode::IteratorStackEmpty); + let empty = self.jump_if_true(); + self.iterator_close(self.in_async_generator); + self.emit(Opcode::Jump, &[start]); + self.patch_jump(empty); + } + + /// Yields from the current generator. + /// + /// This is equivalent to the [`Yield ( value )`][yield] operation from the spec. + /// + /// stack: + /// - value **=>** received + /// + /// [yield]: https://tc39.es/ecma262/#sec-yield + pub(super) fn r#yield(&mut self) { + // 1. Let generatorKind be GetGeneratorKind(). + if self.in_async_generator { + // 2. If generatorKind is async, return ? AsyncGeneratorYield(? Await(value)). + self.emit_opcode(Opcode::Await); + self.emit_opcode(Opcode::GeneratorNext); + self.async_generator_yield(); + } else { + // 3. Otherwise, return ? GeneratorYield(CreateIterResultObject(value, false)). + self.emit_opcode(Opcode::CreateIteratorResult); + self.emit_u8(u8::from(false)); + self.emit_opcode(Opcode::GeneratorYield); + } + self.emit_opcode(Opcode::GeneratorNext); + } + + /// Yields from the current async generator. + /// + /// This is equivalent to the [`AsyncGeneratorYield ( value )`][async_yield] operation from the spec. + /// + /// stack: + /// - value **=>** received + /// + /// [async_yield]: https://tc39.es/ecma262/#sec-asyncgeneratoryield + pub(super) fn async_generator_yield(&mut self) { + self.emit_opcode(Opcode::AsyncGeneratorYield); + let (normal, throw, r#return) = + self.emit_opcode_with_three_operands(Opcode::GeneratorJumpOnResumeKind); + { + self.patch_jump(r#return); + self.emit_opcode(Opcode::Await); + + let (normal, throw, r#return) = + self.emit_opcode_with_three_operands(Opcode::GeneratorJumpOnResumeKind); + self.patch_jump(normal); + self.emit_opcode(Opcode::GeneratorSetReturn); + + self.patch_jump(throw); + self.patch_jump(r#return); + } + self.patch_jump(normal); + self.patch_jump(throw); + } } diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index 7e9a2a1bf6..0b828815ac 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -276,10 +276,6 @@ impl SimpleJobQueue { impl JobQueue for SimpleJobQueue { fn enqueue_promise_job(&self, job: NativeJob, _: &mut Context<'_>) { - // If realm is not null ... - // TODO - // Let scriptOrModule be ... - // TODO self.0.borrow_mut().push_back(job); } diff --git a/boa_engine/src/tests/iterators.rs b/boa_engine/src/tests/iterators.rs new file mode 100644 index 0000000000..eb341f1f98 --- /dev/null +++ b/boa_engine/src/tests/iterators.rs @@ -0,0 +1,222 @@ +use indoc::indoc; + +use crate::{run_test_actions, TestAction}; + +#[test] +fn iterator_close_in_continue_before_jobs() { + run_test_actions([ + TestAction::run_harness(), + TestAction::run(indoc! {r#" + var actual = []; + + var iter = { + [Symbol.iterator]() { + return this; + }, + next() { + actual.push("call next"); + return { + done: false, + }; + }, + get return() { + actual.push("get return"); + return function () { + actual.push("return call"); + return { + done: true + } + } + } + }; + + Promise.resolve(0) + .then(() => actual.push("tick 1")) + .then(() => actual.push("tick 2")); + + void async function f() { + actual.push("async fn start"); + let count = 0; + loop: while (count === 0) { + count++; + for (_ of iter) { + continue loop; + } + } + actual.push("async fn end"); + }(); + "#}), + #[allow(clippy::redundant_closure_for_method_calls)] + TestAction::inspect_context(|ctx| ctx.run_jobs()), + TestAction::assert(indoc! {r#" + arrayEquals( + actual, + [ + "async fn start", + "call next", + "get return", + "return call", + "async fn end", + "tick 1", + "tick 2", + ] + ) + "#}), + ]); +} + +#[test] +fn async_iterator_close_in_continue_is_awaited() { + run_test_actions([ + TestAction::run_harness(), + TestAction::run(indoc! {r#" + var actual = []; + + var asyncIter = { + [Symbol.asyncIterator]() { + return this; + }, + next() { + actual.push("async call next"); + return { + done: false, + }; + }, + get return() { + actual.push("get async return"); + return function () { + actual.push("async return call"); + return { + done: true + }; + } + } + }; + + Promise.resolve(0) + .then(() => actual.push("tick 1")) + .then(() => actual.push("tick 2")) + .then(() => actual.push("tick 3")); + + void async function f() { + actual.push("async fn start"); + let count = 0; + loop: while (count === 0) { + count++; + for await (__ of asyncIter) { + continue loop; + } + } + actual.push("async fn end"); + }(); + "#}), + #[allow(clippy::redundant_closure_for_method_calls)] + TestAction::inspect_context(|ctx| ctx.run_jobs()), + TestAction::assert(indoc! {r#" + arrayEquals( + actual, + [ + "async fn start", + "async call next", + "tick 1", + "get async return", + "async return call", + "tick 2", + "async fn end", + "tick 3" + ] + ) + "#}), + ]); +} + +#[test] +fn mixed_iterators_close_in_continue() { + run_test_actions([ + TestAction::run_harness(), + TestAction::run(indoc! {r#" + var actual = []; + + var iter = { + [Symbol.iterator]() { + return this; + }, + next() { + actual.push("call next"); + return { + done: false, + }; + }, + get return() { + actual.push("get return"); + return function () { + actual.push("return call"); + return { + done: true + } + } + } + }; + + var asyncIter = { + [Symbol.asyncIterator]() { + return this; + }, + next() { + actual.push("async call next"); + return { + done: false, + }; + }, + get return() { + actual.push("get async return"); + return function () { + actual.push("async return call"); + return { + done: true + }; + } + } + }; + + Promise.resolve(0) + .then(() => actual.push("tick 1")) + .then(() => actual.push("tick 2")) + .then(() => actual.push("tick 3")); + + void async function f() { + actual.push("async fn start"); + let count = 0; + loop: while (count === 0) { + count++; + for (_ of iter) { + for await (__ of asyncIter) { + continue loop; + } + } + } + actual.push("async fn end"); + }(); + "#}), + #[allow(clippy::redundant_closure_for_method_calls)] + TestAction::inspect_context(|ctx| ctx.run_jobs()), + TestAction::assert(indoc! {r#" + arrayEquals( + actual, + [ + "async fn start", + "call next", + "async call next", + "tick 1", + "get async return", + "async return call", + "tick 2", + "get return", + "return call", + "async fn end", + "tick 3", + ] + ) + "#}), + ]); +} diff --git a/boa_engine/src/tests/mod.rs b/boa_engine/src/tests/mod.rs index dc4941ea7a..182223e895 100644 --- a/boa_engine/src/tests/mod.rs +++ b/boa_engine/src/tests/mod.rs @@ -3,6 +3,7 @@ use indoc::indoc; mod control_flow; mod env; mod function; +mod iterators; mod operators; mod promise; mod spread; diff --git a/boa_engine/src/vm/call_frame/env_stack.rs b/boa_engine/src/vm/call_frame/env_stack.rs index 8e876fb066..95b1ed7131 100644 --- a/boa_engine/src/vm/call_frame/env_stack.rs +++ b/boa_engine/src/vm/call_frame/env_stack.rs @@ -7,11 +7,14 @@ use boa_gc::{Finalize, Trace}; pub(crate) enum EnvEntryKind { Global, Loop { - /// This is used to keep track of how many iterations a loop has done. + /// How many iterations a loop has done. iteration_count: u64, - // This is the latest return value of the loop. + /// The latest return value of the loop. value: JsValue, + + /// The index of the currently active iterator. + iterator: Option, }, Try, Catch, @@ -71,12 +74,24 @@ impl EnvStackEntry { self } + /// Returns calling `EnvStackEntry` with `kind` field of `Loop`, loop iteration set to zero + /// and iterator index set to `iterator`. + pub(crate) fn with_iterator_loop_flag(mut self, iteration_count: u64, iterator: u32) -> Self { + self.kind = EnvEntryKind::Loop { + iteration_count, + value: JsValue::undefined(), + iterator: Some(iterator), + }; + self + } + /// Returns calling `EnvStackEntry` with `kind` field of `Loop`. /// And the loop iteration set to zero. pub(crate) fn with_loop_flag(mut self, iteration_count: u64) -> Self { self.kind = EnvEntryKind::Loop { iteration_count, value: JsValue::undefined(), + iterator: None, }; self } @@ -150,6 +165,14 @@ impl EnvStackEntry { None } + /// Returns the active iterator index if `EnvStackEntry` is an iterator loop. + pub(crate) const fn iterator(&self) -> Option { + if let EnvEntryKind::Loop { iterator, .. } = self.kind { + return iterator; + } + None + } + /// Returns true if an `EnvStackEntry` is a try block pub(crate) fn is_try_env(&self) -> bool { self.kind == EnvEntryKind::Try diff --git a/boa_engine/src/vm/call_frame/mod.rs b/boa_engine/src/vm/call_frame/mod.rs index b9234c6dd0..8bd151cd07 100644 --- a/boa_engine/src/vm/call_frame/mod.rs +++ b/boa_engine/src/vm/call_frame/mod.rs @@ -6,7 +6,9 @@ mod abrupt_record; mod env_stack; use crate::{ - builtins::promise::PromiseCapability, environments::BindingLocator, object::JsObject, + builtins::{iterable::IteratorRecord, promise::PromiseCapability}, + environments::BindingLocator, + object::JsObject, vm::CodeBlock, }; use boa_gc::{Finalize, Gc, Trace}; @@ -39,7 +41,7 @@ pub struct CallFrame { pub(crate) async_generator: Option, // Iterators and their `[[Done]]` flags that must be closed when an abrupt completion is thrown. - pub(crate) iterators: ThinVec<(JsObject, bool)>, + pub(crate) iterators: ThinVec, // The stack of bindings being updated. pub(crate) binding_stack: Vec, @@ -110,8 +112,9 @@ impl CallFrame { } /// Indicates how a generator function that has been called/resumed should return. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Default)] pub(crate) enum GeneratorResumeKind { + #[default] Normal, Throw, Return, diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index 6aae6bbc6e..ef27b8f4d4 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -317,10 +317,7 @@ impl CodeBlock { | Opcode::Call | Opcode::New | Opcode::SuperCall - | Opcode::IteratorUnwrapNextOrJump - | Opcode::ConcatToString - | Opcode::GeneratorAsyncResumeYield - | Opcode::GeneratorNextDelegate => { + | Opcode::ConcatToString => { let result = self.read::(*pc).to_string(); *pc += size_of::(); result @@ -334,9 +331,10 @@ impl CodeBlock { | Opcode::Break | Opcode::Continue | Opcode::LoopStart + | Opcode::IteratorLoopStart | Opcode::TryStart - | Opcode::AsyncGeneratorNext - | Opcode::GeneratorAsyncDelegateNext => { + | Opcode::GeneratorDelegateNext + | Opcode::GeneratorDelegateResume => { let operand1 = self.read::(*pc); *pc += size_of::(); let operand2 = self.read::(*pc); @@ -350,15 +348,6 @@ impl CodeBlock { *pc += size_of::(); format!("{operand1}, {operand2}") } - Opcode::GeneratorAsyncDelegateResume => { - let operand1 = self.read::(*pc); - *pc += size_of::(); - let operand2 = self.read::(*pc); - *pc += size_of::(); - let operand3 = self.read::(*pc); - *pc += size_of::(); - format!("{operand1}, {operand2}, {operand3}") - } Opcode::GetArrowFunction | Opcode::GetAsyncArrowFunction | Opcode::GetFunction @@ -373,6 +362,7 @@ impl CodeBlock { } Opcode::GetGenerator | Opcode::GetGeneratorAsync => { let operand = self.read::(*pc); + *pc += size_of::(); format!( "{operand:04}: '{}' (length: {})", interner.resolve_expect(self.functions[operand as usize].name), @@ -439,6 +429,20 @@ impl CodeBlock { *pc += size_of::() * (count as usize + 1); String::new() } + Opcode::GeneratorJumpOnResumeKind => { + let normal = self.read::(*pc); + *pc += size_of::(); + let throw = self.read::(*pc); + *pc += size_of::(); + let r#return = self.read::(*pc); + *pc += size_of::(); + format!("n: {normal}, t: {throw}, r: {return}") + } + Opcode::CreateIteratorResult => { + let done = self.read::(*pc) != 0; + *pc += size_of::(); + format!("done: {done}") + } Opcode::Pop | Opcode::PopIfThrown | Opcode::Dup @@ -522,12 +526,14 @@ impl CodeBlock { | Opcode::GetAsyncIterator | Opcode::GeneratorResumeReturn | Opcode::IteratorNext - | Opcode::IteratorNextSetDone - | Opcode::IteratorUnwrapNext - | Opcode::IteratorUnwrapValue + | Opcode::IteratorFinishAsyncNext + | Opcode::IteratorValue + | Opcode::IteratorResult + | Opcode::IteratorDone | Opcode::IteratorToArray - | Opcode::IteratorClosePush - | Opcode::IteratorClosePop + | Opcode::IteratorPop + | Opcode::IteratorReturn + | Opcode::IteratorStackEmpty | Opcode::RequireObjectCoercible | Opcode::ValueNotNullOrUndefined | Opcode::RestParameterInit @@ -538,8 +544,10 @@ impl CodeBlock { | Opcode::PushNewArray | Opcode::PopOnReturnAdd | Opcode::PopOnReturnSub - | Opcode::Yield + | Opcode::GeneratorYield + | Opcode::AsyncGeneratorYield | Opcode::GeneratorNext + | Opcode::GeneratorSetReturn | Opcode::PushClassField | Opcode::SuperCallDerived | Opcode::Await @@ -609,10 +617,7 @@ impl CodeBlock { | Opcode::Reserved50 | Opcode::Reserved51 | Opcode::Reserved52 - | Opcode::Reserved53 - | Opcode::Reserved54 - | Opcode::Reserved55 - | Opcode::Reserved56 => unreachable!("Reserved opcodes are unrechable"), + | Opcode::Reserved53 => unreachable!("Reserved opcodes are unrechable"), } } } diff --git a/boa_engine/src/vm/completion_record.rs b/boa_engine/src/vm/completion_record.rs index 0c307c6f33..a484a84150 100644 --- a/boa_engine/src/vm/completion_record.rs +++ b/boa_engine/src/vm/completion_record.rs @@ -1,18 +1,32 @@ //! An implementation of a `CompletionRecord` for Boa's VM. +use boa_gc::{custom_trace, Finalize, Trace}; + use crate::{JsError, JsResult, JsValue}; /// An implementation of the ECMAScript's `CompletionRecord` [specification] for /// Boa's VM output Completion and Result. /// /// [specification]: https://tc39.es/ecma262/#sec-completion-record-specification-type -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Finalize)] pub(crate) enum CompletionRecord { Normal(JsValue), Return(JsValue), Throw(JsError), } +// SAFETY: this matches all possible variants and traces +// their inner contents, which makes this safe. +unsafe impl Trace for CompletionRecord { + custom_trace!(this, { + match this { + Self::Normal(v) => mark(v), + Self::Return(r) => mark(r), + Self::Throw(th) => mark(th), + } + }); +} + // ---- `CompletionRecord` methods ---- impl CompletionRecord { pub(crate) const fn is_throw_completion(&self) -> bool { diff --git a/boa_engine/src/vm/flowgraph/graph.rs b/boa_engine/src/vm/flowgraph/graph.rs index 13d0ca646c..e82eba9fcf 100644 --- a/boa_engine/src/vm/flowgraph/graph.rs +++ b/boa_engine/src/vm/flowgraph/graph.rs @@ -1,3 +1,8 @@ +use std::{ + collections::hash_map::RandomState, + hash::{BuildHasher, Hash, Hasher}, +}; + use crate::vm::flowgraph::{Color, Edge, EdgeStyle, EdgeType, Node, NodeShape}; /// This represents the direction of flow in the flowgraph. @@ -95,7 +100,10 @@ impl SubGraph { /// Format into the graphviz format. fn graphviz_format(&self, result: &mut String, prefix: &str) { - result.push_str(&format!("\tsubgraph cluster_{prefix}_{} {{\n", self.label)); + let mut hasher = RandomState::new().build_hasher(); + self.label.hash(&mut hasher); + let label = format!("{}", hasher.finish()); + result.push_str(&format!("\tsubgraph cluster_{prefix}_{label} {{\n")); result.push_str("\t\tstyle = filled;\n"); result.push_str(&format!( "\t\tlabel = \"{}\";\n", @@ -107,13 +115,11 @@ impl SubGraph { )); result.push_str(&format!( - "\t\t{prefix}_{}_start [label=\"Start\",shape=Mdiamond,style=filled,color=green]\n", - self.label + "\t\t{prefix}_{label}_start [label=\"Start\",shape=Mdiamond,style=filled,color=green]\n" )); if !self.nodes.is_empty() { result.push_str(&format!( - "\t\t{prefix}_{}_start -> {prefix}_{}_i_0\n", - self.label, self.label + "\t\t{prefix}_{label}_start -> {prefix}_{label}_i_0\n" )); } @@ -126,7 +132,7 @@ impl SubGraph { let color = format!(",style=filled,color=\"{}\"", node.color); result.push_str(&format!( "\t\t{prefix}_{}_i_{}[label=\"{:04}: {}\"{shape}{color}];\n", - self.label, node.location, node.location, node.label + label, node.location, node.location, node.label )); } @@ -142,9 +148,9 @@ impl SubGraph { }; result.push_str(&format!( "\t\t{prefix}_{}_i_{} -> {prefix}_{}_i_{} [label=\"{}\", len=f{style}{color}];\n", - self.label, + label, edge.from, - self.label, + label, edge.to, edge.label.as_deref().unwrap_or("") )); @@ -158,6 +164,9 @@ impl SubGraph { /// Format into the mermaid format. fn mermaid_format(&self, result: &mut String, prefix: &str) { + let mut hasher = RandomState::new().build_hasher(); + self.label.hash(&mut hasher); + let label = format!("{}", hasher.finish()); let rankdir = match self.direction { Direction::TopToBottom => "TB", Direction::BottomToTop => "BT", @@ -166,7 +175,7 @@ impl SubGraph { }; result.push_str(&format!( " subgraph {prefix}_{}[\"{}\"]\n", - self.label, + label, if self.label.is_empty() { "Anonymous Function" } else { @@ -175,15 +184,11 @@ impl SubGraph { )); result.push_str(&format!(" direction {rankdir}\n")); - result.push_str(&format!(" {prefix}_{}_start{{Start}}\n", self.label)); - result.push_str(&format!( - " style {prefix}_{}_start fill:green\n", - self.label - )); + result.push_str(&format!(" {prefix}_{label}_start{{Start}}\n")); + result.push_str(&format!(" style {prefix}_{label}_start fill:green\n")); if !self.nodes.is_empty() { result.push_str(&format!( - " {prefix}_{}_start --> {prefix}_{}_i_0\n", - self.label, self.label + " {prefix}_{label}_start --> {prefix}_{label}_i_0\n" )); } @@ -194,12 +199,12 @@ impl SubGraph { }; result.push_str(&format!( " {prefix}_{}_i_{}{shape_begin}\"{:04}: {}\"{shape_end}\n", - self.label, node.location, node.location, node.label + label, node.location, node.location, node.label )); if !node.color.is_none() { result.push_str(&format!( " style {prefix}_{}_i_{} fill:{}\n", - self.label, node.location, node.color + label, node.location, node.color )); } } @@ -213,10 +218,10 @@ impl SubGraph { }; result.push_str(&format!( " {prefix}_{}_i_{} {style}| {}| {prefix}_{}_i_{}\n", - self.label, + label, edge.from, edge.label.as_deref().unwrap_or(""), - self.label, + label, edge.to, )); diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs index 4bca8be4f1..870437af72 100644 --- a/boa_engine/src/vm/flowgraph/mod.rs +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -59,7 +59,7 @@ impl CodeBlock { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } - Opcode::RotateLeft | Opcode::RotateRight => { + Opcode::RotateLeft | Opcode::RotateRight | Opcode::CreateIteratorResult => { pc += size_of::(); graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); @@ -138,13 +138,9 @@ impl CodeBlock { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::Red); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } - Opcode::LoopStart => { - let start_address = self.read::(pc); - pc += size_of::(); - let end_address = self.read::(pc); - pc += size_of::(); + Opcode::LoopStart | Opcode::IteratorLoopStart => { + pc += size_of::() * 2; - let label = format!("{opcode_str} {start_address}, {end_address}"); graph.add_node(previous_pc, NodeShape::None, label.into(), Color::Red); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } @@ -228,51 +224,57 @@ impl CodeBlock { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, address, None, Color::None, EdgeStyle::Line); } - Opcode::IteratorUnwrapNextOrJump - | Opcode::GeneratorAsyncResumeYield - | Opcode::GeneratorNextDelegate => { - let address = self.read::(pc) as usize; + Opcode::GeneratorDelegateNext => { + let throw_method_undefined = self.read::(pc) as usize; pc += size_of::(); - graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); - graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); - graph.add_edge( + let return_method_undefined = self.read::(pc) as usize; + pc += size_of::(); + graph.add_node( previous_pc, - address, - Some("DONE".into()), + NodeShape::Diamond, + opcode_str.into(), Color::None, - EdgeStyle::Line, ); - } - Opcode::GeneratorAsyncDelegateNext => { - let throw_method_undefined = self.read::(pc) as usize; - let return_method_undefined = self.read::(pc) as usize; - graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); graph.add_edge( previous_pc, throw_method_undefined, - Some("throw method undefined".into()), - Color::None, + Some("`throw` undefined".into()), + Color::Red, EdgeStyle::Line, ); graph.add_edge( previous_pc, return_method_undefined, - Some("return method undefined".into()), - Color::None, + Some("`return` undefined".into()), + Color::Blue, EdgeStyle::Line, ); } - Opcode::GeneratorAsyncDelegateResume => { - self.read::(pc); - self.read::(pc); + Opcode::GeneratorDelegateResume => { + let return_gen = self.read::(pc) as usize; + pc += size_of::(); let exit = self.read::(pc) as usize; pc += size_of::(); - graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_node( + previous_pc, + NodeShape::Diamond, + opcode_str.into(), + Color::None, + ); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + graph.add_edge( + previous_pc, + return_gen, + Some("return".into()), + Color::Yellow, + EdgeStyle::Line, + ); graph.add_edge( previous_pc, exit, - Some("DONE".into()), - Color::None, + Some("done".into()), + Color::Blue, EdgeStyle::Line, ); } @@ -281,31 +283,45 @@ impl CodeBlock { | Opcode::Call | Opcode::New | Opcode::SuperCall - | Opcode::ConcatToString => { + | Opcode::ConcatToString + | Opcode::FinallyStart => { pc += size_of::(); graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } - Opcode::AsyncGeneratorNext => { - let skip_yield = self.read::(pc); + Opcode::GeneratorJumpOnResumeKind => { + let normal = self.read::(pc); + pc += size_of::(); + let throw = self.read::(pc); pc += size_of::(); - let skip_yield_await = self.read::(pc); + let r#return = self.read::(pc); pc += size_of::(); - graph.add_node(previous_pc, NodeShape::None, opcode_str.into(), Color::None); + graph.add_node( + previous_pc, + NodeShape::Diamond, + opcode_str.into(), + Color::None, + ); - graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); graph.add_edge( previous_pc, - skip_yield as usize, - Some("return value pending".into()), + normal as usize, + None, Color::None, EdgeStyle::Line, ); graph.add_edge( previous_pc, - skip_yield_await as usize, - Some("yield value pending".into()), - Color::None, + throw as usize, + Some("throw".into()), + Color::Red, + EdgeStyle::Line, + ); + graph.add_edge( + previous_pc, + r#return as usize, + Some("return".into()), + Color::Yellow, EdgeStyle::Line, ); } @@ -465,7 +481,22 @@ impl CodeBlock { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } - Opcode::Throw | Opcode::ThrowNewTypeError => { + Opcode::ThrowNewTypeError => { + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + if let Some((_try_pc, next, _finally)) = try_entries.last() { + graph.add_edge( + previous_pc, + *next as usize, + Some("CAUGHT".into()), + Color::None, + EdgeStyle::Line, + ); + } else { + returns.push(previous_pc); + } + } + Opcode::Throw => { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); if let Some((_try_pc, next, _finally)) = try_entries.last() { graph.add_edge( @@ -475,6 +506,8 @@ impl CodeBlock { Color::None, EdgeStyle::Line, ); + } else { + returns.push(previous_pc); } } Opcode::PushPrivateEnvironment => { @@ -550,7 +583,6 @@ impl CodeBlock { | Opcode::ToBoolean | Opcode::CatchEnd | Opcode::CatchEnd2 - | Opcode::FinallyStart | Opcode::FinallyEnd | Opcode::This | Opcode::Super @@ -562,12 +594,14 @@ impl CodeBlock { | Opcode::GetIterator | Opcode::GetAsyncIterator | Opcode::IteratorNext - | Opcode::IteratorNextSetDone - | Opcode::IteratorUnwrapNext - | Opcode::IteratorUnwrapValue + | Opcode::IteratorFinishAsyncNext + | Opcode::IteratorValue + | Opcode::IteratorResult + | Opcode::IteratorDone | Opcode::IteratorToArray - | Opcode::IteratorClosePush - | Opcode::IteratorClosePop + | Opcode::IteratorPop + | Opcode::IteratorReturn + | Opcode::IteratorStackEmpty | Opcode::RequireObjectCoercible | Opcode::ValueNotNullOrUndefined | Opcode::RestParameterInit @@ -578,7 +612,8 @@ impl CodeBlock { | Opcode::PushNewArray | Opcode::PopOnReturnAdd | Opcode::PopOnReturnSub - | Opcode::Yield + | Opcode::GeneratorYield + | Opcode::AsyncGeneratorYield | Opcode::GeneratorNext | Opcode::PushClassField | Opcode::SuperCallDerived @@ -596,6 +631,7 @@ impl CodeBlock { | Opcode::PushObjectEnvironment | Opcode::PopPrivateEnvironment | Opcode::ImportCall + | Opcode::GeneratorSetReturn | Opcode::Nop => { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); @@ -674,10 +710,7 @@ impl CodeBlock { | Opcode::Reserved50 | Opcode::Reserved51 | Opcode::Reserved52 - | Opcode::Reserved53 - | Opcode::Reserved54 - | Opcode::Reserved55 - | Opcode::Reserved56 => unreachable!("Reserved opcodes are unrechable"), + | Opcode::Reserved53 => unreachable!("Reserved opcodes are unrechable"), } } diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 50d91ba76a..f9bca1d87a 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -259,8 +259,8 @@ impl Context<'_> { let instant = Instant::now(); let result = self.execute_instruction(); - let duration = instant.elapsed(); + let duration = instant.elapsed(); println!( "{: { Some(value) if value.is_object() => "[object]".to_string(), Some(value) => value.display().to_string(), None => "".to_string(), - } + }, ); result @@ -330,8 +330,8 @@ impl Context<'_> { // Early return immediately after loop. if self.vm.frame().r#yield { - let result = self.vm.pop(); self.vm.frame_mut().r#yield = false; + let result = self.vm.pop(); return CompletionRecord::Return(result); } diff --git a/boa_engine/src/vm/opcode/control_flow/throw.rs b/boa_engine/src/vm/opcode/control_flow/throw.rs index e13f622bf9..fd466104d2 100644 --- a/boa_engine/src/vm/opcode/control_flow/throw.rs +++ b/boa_engine/src/vm/opcode/control_flow/throw.rs @@ -1,9 +1,11 @@ use crate::{ - js_string, - vm::{call_frame::AbruptCompletionRecord, opcode::Operation, CompletionType}, - Context, JsError, JsNativeError, JsResult, + vm::{ + call_frame::{AbruptCompletionRecord, EnvStackEntry}, + opcode::Operation, + CompletionType, + }, + Context, JsError, JsNativeError, JsResult, JsValue, }; -use thin_vec::ThinVec; /// `Throw` implements the Opcode Operation for `Opcode::Throw` /// @@ -23,30 +25,34 @@ impl Operation for Throw { JsError::from_opaque(context.vm.pop()) }; - // Close all iterators that are still open. - let mut iterators = ThinVec::new(); - std::mem::swap(&mut iterators, &mut context.vm.frame_mut().iterators); - for (iterator, done) in iterators { - if done { - continue; - } - if let Ok(Some(f)) = iterator.get_method(js_string!("return"), context) { - drop(f.call(&iterator.into(), &[], context)); - } - } - context.vm.err.take(); - // 1. Find the viable catch and finally blocks let current_address = context.vm.frame().pc; - let viable_catch_candidates = context - .vm - .frame() - .env_stack - .iter() - .filter(|env| env.is_try_env() && env.start_address() < env.exit_address()); + let mut envs = context.vm.frame().env_stack.iter(); + + if let Some(idx) = + envs.rposition(|env| env.is_try_env() && env.start_address() < env.exit_address()) + { + let active_iterator = context.vm.frame().env_stack[..idx] + .iter() + .filter_map(EnvStackEntry::iterator) + .last(); + + // Close all iterators that are outside the catch context. + if let Some(active_iterator) = active_iterator { + let inactive = context + .vm + .frame_mut() + .iterators + .split_off(active_iterator as usize + 1); + for iterator in inactive { + if !iterator.done() { + drop(iterator.close(Ok(JsValue::undefined()), context)); + } + } + context.vm.err.take(); + } - if let Some(candidate) = viable_catch_candidates.last() { - let catch_target = candidate.start_address(); + let catch_target = context.vm.frame().env_stack[idx].start_address(); let mut env_to_pop = 0; let mut target_address = u32::MAX; @@ -126,6 +132,29 @@ impl Operation for Throw { context.vm.frame_mut().env_stack.pop(); } + let active_iterator = context + .vm + .frame() + .env_stack + .iter() + .filter_map(EnvStackEntry::iterator) + .last(); + + // Close all iterators that are outside the finally context. + if let Some(active_iterator) = active_iterator { + let inactive = context + .vm + .frame_mut() + .iterators + .split_off(active_iterator as usize + 1); + for iterator in inactive { + if !iterator.done() { + drop(iterator.close(Ok(JsValue::undefined()), context)); + } + } + context.vm.err.take(); + } + let env_truncation_len = context.vm.environments.len().saturating_sub(env_to_pop); context.vm.environments.truncate(env_truncation_len); @@ -143,6 +172,14 @@ impl Operation for Throw { return Ok(CompletionType::Normal); } + // Close all iterators that are still open. + for iterator in std::mem::take(&mut context.vm.frame_mut().iterators) { + if !iterator.done() { + drop(iterator.close(Ok(JsValue::undefined()), context)); + } + } + context.vm.err.take(); + context.vm.err = Some(error); Ok(CompletionType::Throw) } diff --git a/boa_engine/src/vm/opcode/generator/mod.rs b/boa_engine/src/vm/opcode/generator/mod.rs index a9755c6ae8..d7818946d1 100644 --- a/boa_engine/src/vm/opcode/generator/mod.rs +++ b/boa_engine/src/vm/opcode/generator/mod.rs @@ -1,18 +1,14 @@ pub(crate) mod yield_stm; use crate::{ - builtins::{ - async_generator::{AsyncGenerator, AsyncGeneratorState}, - iterable::IteratorRecord, - }, error::JsNativeError, string::utf16, vm::{ - call_frame::{AbruptCompletionRecord, GeneratorResumeKind}, + call_frame::GeneratorResumeKind, opcode::{control_flow::Return, Operation}, CompletionType, }, - Context, JsError, JsResult, JsValue, + Context, JsError, JsResult, }; pub(crate) use yield_stm::*; @@ -32,115 +28,50 @@ impl Operation for GeneratorNext { match context.vm.frame().generator_resume_kind { GeneratorResumeKind::Normal => Ok(CompletionType::Normal), GeneratorResumeKind::Throw => Err(JsError::from_opaque(context.vm.pop())), - GeneratorResumeKind::Return => { - let finally_entries = context - .vm - .frame() - .env_stack - .iter() - .filter(|entry| entry.is_finally_env()); - if let Some(next_finally) = finally_entries.rev().next() { - if context.vm.frame().pc < next_finally.start_address() { - context.vm.frame_mut().pc = next_finally.start_address(); - let return_record = AbruptCompletionRecord::new_return(); - context.vm.frame_mut().abrupt_completion = Some(return_record); - return Ok(CompletionType::Normal); - } - } - - let return_record = AbruptCompletionRecord::new_return(); - context.vm.frame_mut().abrupt_completion = Some(return_record); - Ok(CompletionType::Return) - } + GeneratorResumeKind::Return => Return::execute(context), } } } -/// `AsyncGeneratorNext` implements the Opcode Operation for `Opcode::AsyncGeneratorNext` +/// `GeneratorJumpOnResumeKind` implements the Opcode Operation for +/// `Opcode::GeneratorJumpOnResumeKind` /// /// Operation: -/// - Resumes the current generator function. +/// - Jumps to the specified instruction if the current resume kind is `Return`. #[derive(Debug, Clone, Copy)] -pub(crate) struct AsyncGeneratorNext; +pub(crate) struct GeneratorJumpOnResumeKind; -impl Operation for AsyncGeneratorNext { - const NAME: &'static str = "AsyncGeneratorNext"; - const INSTRUCTION: &'static str = "INST - AsyncGeneratorNext"; +impl Operation for GeneratorJumpOnResumeKind { + const NAME: &'static str = "GeneratorJumpOnResumeKind"; + const INSTRUCTION: &'static str = "INST - GeneratorJumpOnResumeKind"; fn execute(context: &mut Context<'_>) -> JsResult { - let skip_yield = context.vm.read::(); - let skip_yield_await = context.vm.read::(); - - if context.vm.frame().generator_resume_kind == GeneratorResumeKind::Throw { - return Err(JsError::from_opaque(context.vm.pop())); - } - - let value = context.vm.pop(); - - let completion = Ok(value); - let generator_object = context - .vm - .frame() - .async_generator - .as_ref() - .expect("must be in generator context here") - .clone(); - let next = generator_object - .borrow_mut() - .as_async_generator_mut() - .expect("must be async generator object") - .queue - .pop_front() - .expect("must have item in queue"); - AsyncGenerator::complete_step(&next, completion, false, None, context); - - let mut generator_object_mut = generator_object.borrow_mut(); - let gen = generator_object_mut - .as_async_generator_mut() - .expect("must be async generator object"); - - if let Some(next) = gen.queue.front() { - let (completion, r#return) = &next.completion; - if *r#return { - let value = match completion { - Ok(value) => value.clone(), - Err(e) => e.clone().to_opaque(context), - }; - context.vm.push(value); - context.vm.frame_mut().pc = skip_yield; - } else { - context.vm.push(completion.clone()?); - context.vm.frame_mut().pc = skip_yield_await; - } - } else { - gen.state = AsyncGeneratorState::SuspendedYield; + let normal = context.vm.read::(); + let throw = context.vm.read::(); + let r#return = context.vm.read::(); + match context.vm.frame().generator_resume_kind { + GeneratorResumeKind::Normal => context.vm.frame_mut().pc = normal, + GeneratorResumeKind::Throw => context.vm.frame_mut().pc = throw, + GeneratorResumeKind::Return => context.vm.frame_mut().pc = r#return, } Ok(CompletionType::Normal) } } -/// `GeneratorAsyncResumeYield` implements the Opcode Operation for `Opcode::GeneratorAsyncResumeYield` +/// `GeneratorSetReturn` implements the Opcode Operation for `Opcode::GeneratorSetReturn` /// /// Operation: -/// - Resumes the current async generator function after a yield. +/// - Sets the current generator resume kind to `Return`. #[derive(Debug, Clone, Copy)] -pub(crate) struct GeneratorAsyncResumeYield; +pub(crate) struct GeneratorSetReturn; -impl Operation for GeneratorAsyncResumeYield { - const NAME: &'static str = "GeneratorAsyncResumeYield"; - const INSTRUCTION: &'static str = "INST - GeneratorAsyncResumeYield"; +impl Operation for GeneratorSetReturn { + const NAME: &'static str = "GeneratorSetReturn"; + const INSTRUCTION: &'static str = "INST - GeneratorSetReturn"; fn execute(context: &mut Context<'_>) -> JsResult { - let normal_completion = context.vm.read::(); - - match context.vm.frame().generator_resume_kind { - GeneratorResumeKind::Normal => { - context.vm.frame_mut().pc = normal_completion; - Ok(CompletionType::Normal) - } - GeneratorResumeKind::Throw => Err(JsError::from_opaque(context.vm.pop())), - GeneratorResumeKind::Return => Ok(CompletionType::Normal), - } + context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return; + Ok(CompletionType::Normal) } } @@ -163,135 +94,51 @@ impl Operation for GeneratorResumeReturn { } } -/// `GeneratorNextDelegate` implements the Opcode Operation for `Opcode::GeneratorNextDelegate` -/// -/// Operation: -/// - Delegates the current generator function another generator. -#[derive(Debug, Clone, Copy)] -pub(crate) struct GeneratorNextDelegate; - -impl Operation for GeneratorNextDelegate { - const NAME: &'static str = "GeneratorNextDelegate"; - const INSTRUCTION: &'static str = "INST - GeneratorNextDelegate"; - - fn execute(context: &mut Context<'_>) -> JsResult { - let done_address = context.vm.read::(); - let received = context.vm.pop(); - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let iterator = iterator.as_object().expect("iterator was not an object"); - - match context.vm.frame().generator_resume_kind { - GeneratorResumeKind::Normal => { - let result_value = - next_method.call(&iterator.clone().into(), &[received], context)?; - let result = result_value.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("generator next method returned non-object") - })?; - let done = result.get(utf16!("done"), context)?.to_boolean(); - if done { - context.vm.frame_mut().pc = done_address; - let value = result.get(utf16!("value"), context)?; - context.vm.push(value); - return Ok(CompletionType::Normal); - } - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); - context.vm.push(result_value); - context.vm.frame_mut().r#yield = true; - Ok(CompletionType::Return) - } - GeneratorResumeKind::Throw => { - let throw = iterator.get_method(utf16!("throw"), context)?; - if let Some(throw) = throw { - let result = throw.call(&iterator.clone().into(), &[received], context)?; - let result_object = result.as_object().ok_or_else(|| { - JsNativeError::typ() - .with_message("generator throw method returned non-object") - })?; - let done = result_object.get(utf16!("done"), context)?.to_boolean(); - if done { - context.vm.frame_mut().pc = done_address; - let value = result_object.get(utf16!("value"), context)?; - context.vm.push(value); - return Ok(CompletionType::Normal); - } - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); - context.vm.push(result); - context.vm.frame_mut().r#yield = true; - return Ok(CompletionType::Return); - } - context.vm.frame_mut().pc = done_address; - let iterator_record = IteratorRecord::new(iterator.clone(), next_method, false); - iterator_record.close(Ok(JsValue::Undefined), context)?; - - Err(JsNativeError::typ() - .with_message("iterator does not have a throw method") - .into()) - } - GeneratorResumeKind::Return => { - let r#return = iterator.get_method(utf16!("return"), context)?; - if let Some(r#return) = r#return { - let result = r#return.call(&iterator.clone().into(), &[received], context)?; - let result_object = result.as_object().ok_or_else(|| { - JsNativeError::typ() - .with_message("generator return method returned non-object") - })?; - let done = result_object.get(utf16!("done"), context)?.to_boolean(); - if done { - let value = result_object.get(utf16!("value"), context)?; - context.vm.push(value); - return Return::execute(context); - } - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); - context.vm.push(result); - context.vm.frame_mut().r#yield = true; - return Ok(CompletionType::Return); - } - context.vm.push(received); - Return::execute(context) - } - } - } -} - -/// `GeneratorAsyncDelegateNext` implements the Opcode Operation for `Opcode::GeneratorAsyncDelegateNext` +/// `GeneratorDelegateNext` implements the Opcode Operation for `Opcode::GeneratorDelegateNext` /// /// Operation: -/// - Delegates the current async generator function to another iterator. +/// - Delegates the current generator function to another iterator. #[derive(Debug, Clone, Copy)] -pub(crate) struct GeneratorAsyncDelegateNext; +pub(crate) struct GeneratorDelegateNext; -impl Operation for GeneratorAsyncDelegateNext { - const NAME: &'static str = "GeneratorAsyncDelegateNext"; - const INSTRUCTION: &'static str = "INST - GeneratorAsyncDelegateNext"; +impl Operation for GeneratorDelegateNext { + const NAME: &'static str = "GeneratorDelegateNext"; + const INSTRUCTION: &'static str = "INST - GeneratorDelegateNext"; fn execute(context: &mut Context<'_>) -> JsResult { let throw_method_undefined = context.vm.read::(); let return_method_undefined = context.vm.read::(); let received = context.vm.pop(); - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let iterator = iterator.as_object().expect("iterator was not an object"); - match context.vm.frame().generator_resume_kind { + // Preemptively popping removes the iterator from the iterator stack if any operation + // throws, which avoids calling cleanup operations on the poisoned iterator. + let iterator_record = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator stack should have at least an iterator"); + + match std::mem::take(&mut context.vm.frame_mut().generator_resume_kind) { GeneratorResumeKind::Normal => { - let result_value = - next_method.call(&iterator.clone().into(), &[received], context)?; - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); + let result = iterator_record.next_method().call( + &iterator_record.iterator().clone().into(), + &[received], + context, + )?; context.vm.push(false); - context.vm.push(result_value); - Ok(CompletionType::Normal) + context.vm.push(result); } GeneratorResumeKind::Throw => { - let throw = iterator.get_method(utf16!("throw"), context)?; + let throw = iterator_record + .iterator() + .get_method(utf16!("throw"), context)?; if let Some(throw) = throw { - let result = throw.call(&iterator.clone().into(), &[received], context)?; - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); + let result = throw.call( + &iterator_record.iterator().clone().into(), + &[received], + context, + )?; context.vm.push(false); context.vm.push(result); } else { @@ -299,111 +146,80 @@ impl Operation for GeneratorAsyncDelegateNext { .with_message("iterator does not have a throw method") .to_opaque(context); context.vm.push(error); - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); - context.vm.push(false); context.vm.frame_mut().pc = throw_method_undefined; } - - Ok(CompletionType::Normal) } GeneratorResumeKind::Return => { - let r#return = iterator.get_method(utf16!("return"), context)?; + let r#return = iterator_record + .iterator() + .get_method(utf16!("return"), context)?; if let Some(r#return) = r#return { - let result = r#return.call(&iterator.clone().into(), &[received], context)?; - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); + let result = r#return.call( + &iterator_record.iterator().clone().into(), + &[received], + context, + )?; context.vm.push(true); context.vm.push(result); + } else { + context.vm.push(received); + context.vm.frame_mut().pc = return_method_undefined; + + // The current iterator didn't have a cleanup `return` method, so we can + // skip pushing it to the iterator stack for cleanup. return Ok(CompletionType::Normal); } - context.vm.push(iterator.clone()); - context.vm.push(next_method.clone()); - context.vm.push(received); - context.vm.frame_mut().pc = return_method_undefined; - Ok(CompletionType::Normal) } } + + context.vm.frame_mut().iterators.push(iterator_record); + + Ok(CompletionType::Normal) } } -/// `GeneratorAsyncDelegateResume` implements the Opcode Operation for `Opcode::GeneratorAsyncDelegateResume` +/// `GeneratorDelegateResume` implements the Opcode Operation for `Opcode::GeneratorDelegateResume` /// /// Operation: -/// - Resume the async generator with yield delegate logic after it awaits a value. +/// - Resume the generator with yield delegate logic after it awaits a value. #[derive(Debug, Clone, Copy)] -pub(crate) struct GeneratorAsyncDelegateResume; +pub(crate) struct GeneratorDelegateResume; -impl Operation for GeneratorAsyncDelegateResume { - const NAME: &'static str = "GeneratorAsyncDelegateResume"; - const INSTRUCTION: &'static str = "INST - GeneratorAsyncDelegateResume"; +impl Operation for GeneratorDelegateResume { + const NAME: &'static str = "GeneratorDelegateResume"; + const INSTRUCTION: &'static str = "INST - GeneratorDelegateResume"; fn execute(context: &mut Context<'_>) -> JsResult { - let skip_yield = context.vm.read::(); - let normal_completion = context.vm.read::(); + let return_gen = context.vm.read::(); let exit = context.vm.read::(); + let mut iterator = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator stack should have at least an iterator"); + + let result = context.vm.pop(); + let is_return = context.vm.pop().to_boolean(); + if context.vm.frame().generator_resume_kind == GeneratorResumeKind::Throw { - return Err(JsError::from_opaque(context.vm.pop())); + return Err(JsError::from_opaque(result)); } - let received = context.vm.pop(); - let is_return = context.vm.pop().to_boolean(); + iterator.update_result(result, context)?; - let result = received.as_object().ok_or_else(|| { - JsNativeError::typ().with_message("generator next method returned non-object") - })?; - let done = result.get(utf16!("done"), context)?.to_boolean(); - let value = result.get(utf16!("value"), context)?; - if done { + if iterator.done() { + let value = iterator.value(context)?; context.vm.push(value); - if is_return { - return Return::execute(context); - } + context.vm.frame_mut().pc = if is_return { return_gen } else { exit }; - context.vm.frame_mut().pc = exit; return Ok(CompletionType::Normal); } - let completion = Ok(value); - let generator_object = context - .vm - .frame() - .async_generator - .as_ref() - .expect("must be in generator context here") - .clone(); - let next = generator_object - .borrow_mut() - .as_async_generator_mut() - .expect("must be async generator object") - .queue - .pop_front() - .expect("must have item in queue"); - AsyncGenerator::complete_step(&next, completion, false, None, context); - - let mut generator_object_mut = generator_object.borrow_mut(); - let gen = generator_object_mut - .as_async_generator_mut() - .expect("must be async generator object"); - - if let Some(next) = gen.queue.front() { - let (completion, r#return) = &next.completion; - if *r#return { - let value = match completion { - Ok(value) => value.clone(), - Err(e) => e.clone().to_opaque(context), - }; - context.vm.push(value); - context.vm.frame_mut().pc = skip_yield; - } else { - context.vm.push(completion.clone()?); - context.vm.frame_mut().pc = normal_completion; - } - } else { - gen.state = AsyncGeneratorState::SuspendedYield; - } + context.vm.frame_mut().iterators.push(iterator); + Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/generator/yield_stm.rs b/boa_engine/src/vm/opcode/generator/yield_stm.rs index e70b71a556..8f545de58d 100644 --- a/boa_engine/src/vm/opcode/generator/yield_stm.rs +++ b/boa_engine/src/vm/opcode/generator/yield_stm.rs @@ -1,25 +1,87 @@ use crate::{ - builtins::iterable::create_iter_result_object, - vm::{opcode::Operation, CompletionType}, - Context, JsResult, + builtins::async_generator::{AsyncGenerator, AsyncGeneratorState}, + vm::{opcode::Operation, CompletionRecord, CompletionType, GeneratorResumeKind}, + Context, JsResult, JsValue, }; -/// `Yield` implements the Opcode Operation for `Opcode::Yield` +/// `GeneratorYield` implements the Opcode Operation for `Opcode::GeneratorYield` /// /// Operation: -/// - Yield from the current execution. +/// - Yield from the current generator execution. #[derive(Debug, Clone, Copy)] -pub(crate) struct Yield; +pub(crate) struct GeneratorYield; -impl Operation for Yield { - const NAME: &'static str = "Yield"; - const INSTRUCTION: &'static str = "INST - Yield"; +impl Operation for GeneratorYield { + const NAME: &'static str = "GeneratorYield"; + const INSTRUCTION: &'static str = "INST - GeneratorYield"; fn execute(context: &mut Context<'_>) -> JsResult { - let value = context.vm.pop(); - let value = create_iter_result_object(value, false, context); - context.vm.push(value); context.vm.frame_mut().r#yield = true; Ok(CompletionType::Return) } } + +/// `AsyncGeneratorYield` implements the Opcode Operation for `Opcode::AsyncGeneratorYield` +/// +/// Operation: +/// - Yield from the current async generator execution. +#[derive(Debug, Clone, Copy)] +pub(crate) struct AsyncGeneratorYield; + +impl Operation for AsyncGeneratorYield { + const NAME: &'static str = "AsyncGeneratorYield"; + const INSTRUCTION: &'static str = "INST - AsyncGeneratorYield"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let value = context.vm.pop(); + + let async_gen = context + .vm + .frame() + .async_generator + .clone() + .expect("`AsyncGeneratorYield` must only be called inside async generators"); + let completion = Ok(value); + let next = async_gen + .borrow_mut() + .as_async_generator_mut() + .expect("must be async generator object") + .queue + .pop_front() + .expect("must have item in queue"); + + // TODO: 7. Let previousContext be the second to top element of the execution context stack. + AsyncGenerator::complete_step(&next, completion, false, None, context); + + let mut generator_object_mut = async_gen.borrow_mut(); + let gen = generator_object_mut + .as_async_generator_mut() + .expect("must be async generator object"); + + if let Some(next) = gen.queue.front() { + let resume_kind = match next.completion.clone() { + CompletionRecord::Normal(val) => { + context.vm.push(val); + GeneratorResumeKind::Normal + } + CompletionRecord::Return(val) => { + context.vm.push(val); + GeneratorResumeKind::Return + } + CompletionRecord::Throw(err) => { + let err = err.to_opaque(context); + context.vm.push(err); + GeneratorResumeKind::Throw + } + }; + + context.vm.frame_mut().generator_resume_kind = resume_kind; + + Ok(CompletionType::Normal) + } else { + gen.state = AsyncGeneratorState::SuspendedYield; + context.vm.push(JsValue::undefined()); + GeneratorYield::execute(context) + } + } +} diff --git a/boa_engine/src/vm/opcode/iteration/for_in.rs b/boa_engine/src/vm/opcode/iteration/for_in.rs index a781a98395..443e290b72 100644 --- a/boa_engine/src/vm/opcode/iteration/for_in.rs +++ b/boa_engine/src/vm/opcode/iteration/for_in.rs @@ -1,5 +1,5 @@ use crate::{ - builtins::object::for_in_iterator::ForInIterator, + builtins::{iterable::IteratorRecord, object::for_in_iterator::ForInIterator}, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsValue, }; @@ -24,8 +24,12 @@ impl Operation for CreateForInIterator { .get("next", context) .expect("ForInIterator must have a `next` method"); - context.vm.push(iterator); - context.vm.push(next_method); + context + .vm + .frame_mut() + .iterators + .push(IteratorRecord::new(iterator, next_method)); + Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/iteration/get.rs b/boa_engine/src/vm/opcode/iteration/get.rs index 0b68663c1d..649a8e609e 100644 --- a/boa_engine/src/vm/opcode/iteration/get.rs +++ b/boa_engine/src/vm/opcode/iteration/get.rs @@ -18,8 +18,7 @@ impl Operation for GetIterator { fn execute(context: &mut Context<'_>) -> JsResult { let object = context.vm.pop(); let iterator = object.get_iterator(context, None, None)?; - context.vm.push(iterator.iterator().clone()); - context.vm.push(iterator.next_method().clone()); + context.vm.frame_mut().iterators.push(iterator); Ok(CompletionType::Normal) } } @@ -38,8 +37,7 @@ impl Operation for GetAsyncIterator { fn execute(context: &mut Context<'_>) -> JsResult { let object = context.vm.pop(); let iterator = object.get_iterator(context, Some(IteratorHint::Async), None)?; - context.vm.push(iterator.iterator().clone()); - context.vm.push(iterator.next_method().clone()); + context.vm.frame_mut().iterators.push(iterator); Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/iteration/iterator.rs b/boa_engine/src/vm/opcode/iteration/iterator.rs index b99f1af586..1a7b06deb7 100644 --- a/boa_engine/src/vm/opcode/iteration/iterator.rs +++ b/boa_engine/src/vm/opcode/iteration/iterator.rs @@ -1,16 +1,16 @@ +use std::matches; + use crate::{ - builtins::{ - iterable::{IteratorRecord, IteratorResult}, - Array, - }, - vm::{opcode::Operation, CompletionType}, - Context, JsNativeError, JsResult, + builtins::{iterable::create_iter_result_object, Array}, + js_string, + vm::{opcode::Operation, CompletionType, GeneratorResumeKind}, + Context, JsResult, }; /// `IteratorNext` implements the Opcode Operation for `Opcode::IteratorNext` /// /// Operation: -/// - Calls the `next` method of `iterator` and puts its return value on the stack. +/// - Calls the `next` method of `iterator`, updating its record with the next value. #[derive(Debug, Clone, Copy)] pub(crate) struct IteratorNext; @@ -19,149 +19,171 @@ impl Operation for IteratorNext { const INSTRUCTION: &'static str = "INST - IteratorNext"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let next_result = next_method.call(&iterator, &[], context)?; - context.vm.push(iterator); - context.vm.push(next_method); - context.vm.push(next_result); + let mut iterator = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator stack should have at least an iterator"); + + iterator.step(context)?; + + context.vm.frame_mut().iterators.push(iterator); + Ok(CompletionType::Normal) } } -/// `IteratorNextSetDone` implements the Opcode Operation for `Opcode::IteratorNextSetDone` +/// `IteratorFinishAsyncNext` implements the Opcode Operation for `Opcode::IteratorFinishAsyncNext`. /// /// Operation: -/// - Calls the `next` method of `iterator`, puts its return value on the stack -/// and sets the `[[Done]]` value of the iterator on the call frame. +/// - Finishes the call to `Opcode::IteratorNext` within a `for await` loop by setting the current +/// result of the current iterator. #[derive(Debug, Clone, Copy)] -pub(crate) struct IteratorNextSetDone; +pub(crate) struct IteratorFinishAsyncNext; -impl Operation for IteratorNextSetDone { - const NAME: &'static str = "IteratorNextSetDone"; - const INSTRUCTION: &'static str = "INST - IteratorNextSetDone"; +impl Operation for IteratorFinishAsyncNext { + const NAME: &'static str = "IteratorFinishAsyncNext"; + const INSTRUCTION: &'static str = "INST - IteratorFinishAsyncNext"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let mut done = true; - let result = next_method - .call(&iterator, &[], context) - .and_then(|next_result| { - next_method - .as_object() - .cloned() - .map(IteratorResult::new) - .ok_or_else(|| { - JsNativeError::typ() - .with_message("next value should be an object") - .into() - }) - .and_then(|iterator_result| { - iterator_result.complete(context).map(|d| { - done = d; - context.vm.push(iterator); - context.vm.push(next_method); - context.vm.push(next_result); - CompletionType::Normal - }) - }) - }); - - context + let mut iterator = context .vm .frame_mut() .iterators - .last_mut() + .pop() + .expect("iterator on the call frame must exist"); + + if matches!( + context.vm.frame().generator_resume_kind, + GeneratorResumeKind::Throw + ) { + // If after awaiting the `next` call the iterator returned an error, it can be considered + // as poisoned, meaning we can remove it from the iterator stack to avoid calling + // cleanup operations on it. + return Ok(CompletionType::Normal); + } + + let next_result = context.vm.pop(); + + iterator.update_result(next_result, context)?; + + context.vm.frame_mut().iterators.push(iterator); + Ok(CompletionType::Normal) + } +} + +/// `IteratorResult` implements the Opcode Operation for `Opcode::IteratorResult` +/// +/// Operation: +/// - Gets the last iteration result of the current iterator record. +#[derive(Debug, Clone, Copy)] +pub(crate) struct IteratorResult; + +impl Operation for IteratorResult { + const NAME: &'static str = "IteratorResult"; + const INSTRUCTION: &'static str = "INST - IteratorResult"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let last_result = context + .vm + .frame() + .iterators + .last() .expect("iterator on the call frame must exist") - .1 = done; + .last_result() + .object() + .clone(); + + context.vm.push(last_result); - result + Ok(CompletionType::Normal) } } -/// `IteratorUnwrapNext` implements the Opcode Operation for `Opcode::IteratorUnwrapNext` +/// `IteratorValue` implements the Opcode Operation for `Opcode::IteratorValue` /// /// Operation: -/// - Gets the `value` and `done` properties of an iterator result. +/// - Gets the `value` property of the current iterator record. #[derive(Debug, Clone, Copy)] -pub(crate) struct IteratorUnwrapNext; +pub(crate) struct IteratorValue; -impl Operation for IteratorUnwrapNext { - const NAME: &'static str = "IteratorUnwrapNext"; - const INSTRUCTION: &'static str = "INST - IteratorUnwrapNext"; +impl Operation for IteratorValue { + const NAME: &'static str = "IteratorValue"; + const INSTRUCTION: &'static str = "INST - IteratorValue"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_result = context.vm.pop(); - let next_result = next_result - .as_object() - .cloned() - .map(IteratorResult::new) - .ok_or_else(|| JsNativeError::typ().with_message("next value should be an object"))?; - let complete = next_result.complete(context)?; - let value = next_result.value(context)?; - context.vm.push(complete); + let mut iterator = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator on the call frame must exist"); + + let value = iterator.value(context)?; context.vm.push(value); + context.vm.frame_mut().iterators.push(iterator); + Ok(CompletionType::Normal) } } -/// `IteratorUnwrapValue` implements the Opcode Operation for `Opcode::IteratorUnwrapValue` +/// `IteratorDone` implements the Opcode Operation for `Opcode::IteratorDone` /// /// Operation: -/// - Gets the `value` property of an iterator result. +/// - Returns `true` if the current iterator is done, or `false` otherwise #[derive(Debug, Clone, Copy)] -pub(crate) struct IteratorUnwrapValue; +pub(crate) struct IteratorDone; -impl Operation for IteratorUnwrapValue { - const NAME: &'static str = "IteratorUnwrapValue"; - const INSTRUCTION: &'static str = "INST - IteratorUnwrapValue"; +impl Operation for IteratorDone { + const NAME: &'static str = "IteratorDone"; + const INSTRUCTION: &'static str = "INST - IteratorDone"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_result = context.vm.pop(); - let next_result = next_result - .as_object() - .cloned() - .map(IteratorResult::new) - .ok_or_else(|| JsNativeError::typ().with_message("next value should be an object"))?; - let value = next_result.value(context)?; - context.vm.push(value); + let done = context + .vm + .frame() + .iterators + .last() + .expect("iterator on the call frame must exist") + .done(); + + context.vm.push(done); Ok(CompletionType::Normal) } } -/// `IteratorUnwrapNextOrJump` implements the Opcode Operation for `Opcode::IteratorUnwrapNextOrJump` +/// `IteratorReturn` implements the Opcode Operation for `Opcode::IteratorReturn` /// /// Operation: -/// - Gets the `value` and `done` properties of an iterator result, or jump to `address` if -/// `done` is true. +/// - Calls `return` on the current iterator and returns the result. #[derive(Debug, Clone, Copy)] -pub(crate) struct IteratorUnwrapNextOrJump; +pub(crate) struct IteratorReturn; -impl Operation for IteratorUnwrapNextOrJump { - const NAME: &'static str = "IteratorUnwrapNextOrJump"; - const INSTRUCTION: &'static str = "INST - IteratorUnwrapNextOrJump"; +impl Operation for IteratorReturn { + const NAME: &'static str = "IteratorReturn"; + const INSTRUCTION: &'static str = "INST - IteratorReturn"; fn execute(context: &mut Context<'_>) -> JsResult { - let address = context.vm.read::(); + let record = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator on the call frame must exist"); - let next_result = context.vm.pop(); - let next_result = next_result - .as_object() - .cloned() - .map(IteratorResult::new) - .ok_or_else(|| JsNativeError::typ().with_message("next value should be an object"))?; - - if next_result.complete(context)? { - context.vm.frame_mut().pc = address; - context.vm.push(true); - } else { + let Some(ret) = record.iterator().get_method(js_string!("return"), context)? else { context.vm.push(false); - let value = next_result.value(context)?; - context.vm.push(value); - } + return Ok(CompletionType::Normal); + }; + + let value = ret.call(&record.iterator().clone().into(), &[], context)?; + + context.vm.push(value); + context.vm.push(true); + Ok(CompletionType::Normal) } } @@ -178,83 +200,101 @@ impl Operation for IteratorToArray { const INSTRUCTION: &'static str = "INST - IteratorToArray"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let iterator = iterator.as_object().expect("iterator was not an object"); + let mut iterator = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator on the call frame must exist"); - let iterator_record = IteratorRecord::new(iterator.clone(), next_method.clone(), false); let mut values = Vec::new(); - let err = loop { - match iterator_record.step(context) { - Ok(Some(result)) => match result.value(context) { - Ok(value) => values.push(value), - Err(err) => break Some(err), - }, - Ok(None) => break None, - Err(err) => break Some(err), + loop { + let done = match iterator.step(context) { + Ok(done) => done, + Err(err) => { + context.vm.frame_mut().iterators.push(iterator); + return Err(err); + } + }; + + if done { + break; } - }; - context - .vm - .frame_mut() - .iterators - .last_mut() - .expect("should exist") - .1 = true; - if let Some(err) = err { - return Err(err); + match iterator.value(context) { + Ok(value) => values.push(value), + Err(err) => { + context.vm.frame_mut().iterators.push(iterator); + return Err(err); + } + } } + context.vm.frame_mut().iterators.push(iterator); + let array = Array::create_array_from_list(values, context); - context.vm.push(iterator.clone()); - context.vm.push(next_method); context.vm.push(array); + Ok(CompletionType::Normal) } } -/// `IteratorClosePush` implements the Opcode Operation for `Opcode::IteratorClosePush` +/// `IteratorPop` implements the Opcode Operation for `Opcode::IteratorPop` /// /// Operation: -/// - Push an iterator to the call frame close iterator stack. +/// - Pop an iterator from the call frame close iterator stack. #[derive(Debug, Clone, Copy)] -pub(crate) struct IteratorClosePush; +pub(crate) struct IteratorPop; -impl Operation for IteratorClosePush { - const NAME: &'static str = "IteratorClosePush"; - const INSTRUCTION: &'static str = "INST - IteratorClosePush"; +impl Operation for IteratorPop { + const NAME: &'static str = "IteratorPop"; + const INSTRUCTION: &'static str = "INST - IteratorPop"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let iterator_object = iterator.as_object().expect("iterator was not an object"); - context - .vm - .frame_mut() - .iterators - .push((iterator_object.clone(), false)); - context.vm.push(iterator); - context.vm.push(next_method); + context.vm.frame_mut().iterators.pop(); Ok(CompletionType::Normal) } } -/// `IteratorClosePop` implements the Opcode Operation for `Opcode::IteratorClosePop` +/// `IteratorStackEmpty` implements the Opcode Operation for `Opcode::IteratorStackEmpty` /// /// Operation: -/// - Pop an iterator from the call frame close iterator stack. +/// - Pushes `true` to the stack if the iterator stack is empty. #[derive(Debug, Clone, Copy)] -pub(crate) struct IteratorClosePop; +pub(crate) struct IteratorStackEmpty; -impl Operation for IteratorClosePop { - const NAME: &'static str = "IteratorClosePop"; - const INSTRUCTION: &'static str = "INST - IteratorClosePop"; +impl Operation for IteratorStackEmpty { + const NAME: &'static str = "IteratorStackEmpty"; + const INSTRUCTION: &'static str = "INST - IteratorStackEmpty"; fn execute(context: &mut Context<'_>) -> JsResult { - context.vm.frame_mut().iterators.pop(); + let is_empty = context.vm.frame().iterators.is_empty(); + context.vm.push(is_empty); + Ok(CompletionType::Normal) + } +} + +/// `CreateIteratorResult` implements the Opcode Operation for `Opcode::CreateIteratorResult` +/// +/// Operation: +/// - Creates a new iterator result object +#[derive(Debug, Clone, Copy)] +pub(crate) struct CreateIteratorResult; + +impl Operation for CreateIteratorResult { + const NAME: &'static str = "CreateIteratorResult"; + const INSTRUCTION: &'static str = "INST - CreateIteratorResult"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let value = context.vm.pop(); + let done = context.vm.read::() != 0; + + let result = create_iter_result_object(value, done, context); + + context.vm.push(result); + Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/iteration/loop_ops.rs b/boa_engine/src/vm/opcode/iteration/loop_ops.rs index fdacf20903..dee19bac82 100644 --- a/boa_engine/src/vm/opcode/iteration/loop_ops.rs +++ b/boa_engine/src/vm/opcode/iteration/loop_ops.rs @@ -4,6 +4,29 @@ use crate::{ Context, JsResult, }; +/// `IteratorLoopStart` implements the Opcode Operation for `Opcode::IteratorLoopStart` +/// +/// Operation: +/// - Push iterator loop start marker. +#[derive(Debug, Clone, Copy)] +pub(crate) struct IteratorLoopStart; + +impl Operation for IteratorLoopStart { + const NAME: &'static str = "IteratorLoopStart"; + const INSTRUCTION: &'static str = "INST - IteratorLoopStart"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let start = context.vm.read::(); + let exit = context.vm.read::(); + + // Create and push loop evironment entry. + let entry = EnvStackEntry::new(start, exit) + .with_iterator_loop_flag(1, (context.vm.frame().iterators.len() - 1) as u32); + context.vm.frame_mut().env_stack.push(entry); + Ok(CompletionType::Normal) + } +} + /// `LoopStart` implements the Opcode Operation for `Opcode::LoopStart` /// /// Operation: diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index a4827bbfbc..038d899d4d 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -1379,7 +1379,9 @@ generate_impl! { /// Push loop start marker. /// - /// Operands: Exit Address: `u32` + /// Operands: + /// - start: `u32` + /// - exit: `u32` /// /// Stack: **=>** LoopStart, @@ -1421,82 +1423,122 @@ generate_impl! { /// Creates the ForInIterator of an object. /// - /// Stack: object **=>** iterator, next_method + /// Stack: object **=>** + /// + /// Iterator Stack: `iterator` CreateForInIterator, + /// Push iterator loop start marker. + /// + /// Operands: + /// - start: `u32` + /// - exit: `u32` + /// + /// Stack: **=>** + IteratorLoopStart, + /// Gets the iterator of an object. /// /// Operands: /// - /// Stack: object **=>** iterator, next_method + /// Stack: object **=>** + /// + /// Iterator Stack: `iterator` GetIterator, /// Gets the async iterator of an object. /// /// Operands: /// - /// Stack: object **=>** iterator, next_method + /// Stack: object **=>** + /// + /// Iterator Stack: `iterator` GetAsyncIterator, - /// Calls the `next` method of `iterator` and puts its return value on the stack. + /// Calls the `next` method of `iterator`, updating its record with the next value. /// /// Operands: /// - /// Stack: iterator, next_method **=>** iterator, next_method, next_value + /// Iterator Stack: `iterator` **=>** `iterator` IteratorNext, - /// Calls the `next` method of `iterator`, puts its return value on the stack - /// and sets the `[[Done]]` value of the iterator on the call frame. + /// Returns `true` if the current iterator is done, or `false` otherwise /// - /// Operands: + /// Stack: **=>** done /// - /// Stack: iterator, next_method **=>** iterator, next_method, next_value - IteratorNextSetDone, + /// Iterator Stack: `iterator` **=>** `iterator` + IteratorDone, - /// Gets the `value` and `done` properties of an iterator result. + /// Finishes the call to `Opcode::IteratorNext` within a `for await` loop by setting the current + /// result of the current iterator. + /// + /// Operands: /// - /// Stack: next_result **=>** done, next_value - IteratorUnwrapNext, + /// Stack: `next_result` **=>** + /// + /// Iterator Stack: iterator **=>** iterator + IteratorFinishAsyncNext, - /// Gets the `value` property of an iterator result. + /// - Gets the `value` property of the current iterator record. + /// + /// Stack: **=>** `value` /// - /// Stack: next_result **=>** next_value - IteratorUnwrapValue, + /// Iterator Stack: `iterator` **=>** `iterator` + IteratorValue, - /// Gets the `value` and `done` properties of an iterator result, or jump to `address` if - /// `done` is true. + /// - Gets the last iteration result of the current iterator record. /// - /// Operands: address: `u32` + /// Stack: **=>** `result` /// - /// Stack: next_result **=>** done, next_value ( if done != true ) - IteratorUnwrapNextOrJump, + /// Iterator Stack: `iterator` **=>** `iterator` + IteratorResult, /// Consume the iterator and construct and array with all the values. /// /// Operands: /// - /// Stack: iterator, next_method **=>** iterator, next_method, array + /// Stack: **=>** array + /// + /// Iterator Stack: `iterator` **=>** `iterator` IteratorToArray, - /// Push an iterator to the call frame close iterator stack. + /// Pop an iterator from the call frame close iterator stack. + /// + /// Iterator Stack: + /// - `iterator` **=>** + IteratorPop, + + /// Pushes `true` to the stack if the iterator stack is empty. /// - /// Operands: + /// Stack: + /// - **=>** `is_empty` /// - /// Stack: iterator, next_method => iterator, next_method - IteratorClosePush, + /// Iterator Stack: + /// - **=>** + IteratorStackEmpty, - /// Pop an iterator from the call frame close iterator stack. + /// Creates a new iterator result object. /// /// Operands: + /// - done: bool (codified as u8 with `0` -> `false` and `!0` -> `true`) /// /// Stack: - IteratorClosePop, + /// - value **=>** + /// + CreateIteratorResult, + + /// Calls `return` on the current iterator and returns the result. + /// + /// Stack: **=>** return_val (if return is a method), is_return_method + /// + /// Iterator Stack: `iterator` **=>** + IteratorReturn, /// Concat multiple stack objects into a string. /// /// Operands: value_count: `u32` /// - /// Stack: value_1,...value_n **=>** string + /// Stack: `value_1`,...`value_n` **=>** `string` ConcatToString, /// Call RequireObjectCoercible on the stack value. @@ -1541,12 +1583,12 @@ generate_impl! { /// Stack: **=>** PopOnReturnSub, - /// Yield from the current execution. + /// Yields from the current generator execution. /// /// Operands: /// - /// Stack: value **=>** - Yield, + /// Stack: value **=>** received + GeneratorYield, /// Resumes the current generator function. /// @@ -1562,46 +1604,49 @@ generate_impl! { /// Stack: **=>** GeneratorResumeReturn, - /// Resumes the current generator function. + /// Yields from the current async generator execution. /// - /// Operands: skip_yield: u32, skip_yield_await: u32 + /// Operands: /// - /// Stack: received **=>** `Option` - AsyncGeneratorNext, + /// Stack: value **=>** received + AsyncGeneratorYield, - /// Resumes the current async generator function after a yield. + /// Jumps to the specified instruction for each resume kind. /// - /// Operands: normal_completion: u32 + /// Operands: + /// - normal: u32, + /// - throw: u32, + /// - return: u32, /// - /// Stack: **=>** - GeneratorAsyncResumeYield, + /// Stack: + GeneratorJumpOnResumeKind, - /// Delegates the current generator function to another iterator. + /// Sets the current generator resume kind to `Return`. /// - /// Operands: done_address: `u32` + /// Operands: /// - /// Stack: iterator, next_method, received **=>** iterator, next_method - GeneratorNextDelegate, + /// Stack: + GeneratorSetReturn, /// Delegates the current async generator function to another iterator. /// /// Operands: throw_method_undefined: `u32`, return_method_undefined: `u32` /// - /// Stack: iterator, next_method, received **=>** iterator, next_method, is_return, result - GeneratorAsyncDelegateNext, + /// Stack: received **=>** result + GeneratorDelegateNext, /// Resume the async generator with yield delegate logic after it awaits a value. /// - /// Operands: skip_yield: `u32`, normal_completion: `u32`, exit: `u32` + /// Operands: return: `u32`, exit: `u32` /// /// Stack: is_return, received **=>** value - GeneratorAsyncDelegateResume, + GeneratorDelegateResume, /// Stops the current async function and schedules it to resume later. /// /// Operands: /// - /// Stack: promise **=>** + /// Stack: promise **=>** received Await, /// Push the current new target to the stack. @@ -1766,12 +1811,6 @@ generate_impl! { Reserved52 => Reserved, /// Reserved [`Opcode`]. Reserved53 => Reserved, - /// Reserved [`Opcode`]. - Reserved54 => Reserved, - /// Reserved [`Opcode`]. - Reserved55 => Reserved, - /// Reserved [`Opcode`]. - Reserved56 => Reserved, } } diff --git a/boa_engine/src/vm/opcode/push/array.rs b/boa_engine/src/vm/opcode/push/array.rs index db5b40334b..3783a10c6c 100644 --- a/boa_engine/src/vm/opcode/push/array.rs +++ b/boa_engine/src/vm/opcode/push/array.rs @@ -1,5 +1,5 @@ use crate::{ - builtins::{iterable::IteratorRecord, Array}, + builtins::Array, object::ObjectData, string::utf16, vm::{opcode::Operation, CompletionType}, @@ -90,15 +90,17 @@ impl Operation for PushIteratorToArray { const INSTRUCTION: &'static str = "INST - PushIteratorToArray"; fn execute(context: &mut Context<'_>) -> JsResult { - let next_method = context.vm.pop(); - let iterator = context.vm.pop(); - let iterator = iterator.as_object().expect("iterator was not an object"); + let mut iterator = context + .vm + .frame_mut() + .iterators + .pop() + .expect("iterator stack should have at least an iterator"); let array = context.vm.pop(); - let iterator = IteratorRecord::new(iterator.clone(), next_method, false); - while let Some(next) = iterator.step(context)? { - let next_value = next.value(context)?; - Array::push(&array, &[next_value], context)?; + while !iterator.step(context)? { + let next = iterator.value(context)?; + Array::push(&array, &[next], context)?; } context.vm.push(array);