mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
415 lines
16 KiB
415 lines
16 KiB
//! This module implements the global `Generator` object. |
|
//! |
|
//! A Generator is an instance of a generator function and conforms to both the Iterator and Iterable interfaces. |
|
//! |
|
//! More information: |
|
//! - [ECMAScript reference][spec] |
|
//! - [MDN documentation][mdn] |
|
//! |
|
//! [spec]: https://tc39.es/ecma262/#sec-generator-objects |
|
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator |
|
|
|
use crate::{ |
|
builtins::{iterable::create_iter_result_object, BuiltIn, JsArgs}, |
|
environments::DeclarativeEnvironmentStack, |
|
object::{ConstructorBuilder, JsObject, ObjectData}, |
|
property::{Attribute, PropertyDescriptor}, |
|
symbol::WellKnownSymbols, |
|
value::JsValue, |
|
vm::{CallFrame, GeneratorResumeKind, ReturnType}, |
|
Context, JsResult, |
|
}; |
|
use boa_gc::{Cell, Finalize, Gc, Trace}; |
|
use boa_profiler::Profiler; |
|
|
|
/// Indicates the state of a generator. |
|
#[derive(Debug, Clone, Copy, PartialEq)] |
|
pub(crate) enum GeneratorState { |
|
Undefined, |
|
SuspendedStart, |
|
SuspendedYield, |
|
Executing, |
|
Completed, |
|
} |
|
|
|
/// Holds all information that a generator needs to continue it's execution. |
|
/// |
|
/// All of the fields must be changed with those that are currently present in the |
|
/// context/vm before the generator execution starts/resumes and after it has ended/yielded. |
|
#[derive(Debug, Clone, Finalize, Trace)] |
|
pub(crate) struct GeneratorContext { |
|
pub(crate) environments: DeclarativeEnvironmentStack, |
|
pub(crate) call_frame: CallFrame, |
|
pub(crate) stack: Vec<JsValue>, |
|
} |
|
|
|
/// The internal representation on a `Generator` object. |
|
#[derive(Debug, Clone, Finalize, Trace)] |
|
pub struct Generator { |
|
/// The `[[GeneratorState]]` internal slot. |
|
#[unsafe_ignore_trace] |
|
pub(crate) state: GeneratorState, |
|
|
|
/// The `[[GeneratorContext]]` internal slot. |
|
pub(crate) context: Option<Gc<Cell<GeneratorContext>>>, |
|
} |
|
|
|
impl BuiltIn for Generator { |
|
const NAME: &'static str = "Generator"; |
|
|
|
const ATTRIBUTE: Attribute = Attribute::NON_ENUMERABLE.union(Attribute::CONFIGURABLE); |
|
|
|
fn init(context: &mut Context) -> JsValue { |
|
let _timer = Profiler::global().start_event(Self::NAME, "init"); |
|
|
|
let iterator_prototype = context.iterator_prototypes().iterator_prototype(); |
|
|
|
let generator_function_prototype = context |
|
.standard_objects() |
|
.generator_function_object() |
|
.prototype(); |
|
|
|
let obj = ConstructorBuilder::with_standard_object( |
|
context, |
|
Self::constructor, |
|
context.standard_objects().generator_object().clone(), |
|
) |
|
.name(Self::NAME) |
|
.length(Self::LENGTH) |
|
.property( |
|
WellKnownSymbols::to_string_tag(), |
|
Self::NAME, |
|
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
|
) |
|
.method(Self::next, "next", 1) |
|
.method(Self::r#return, "return", 1) |
|
.method(Self::throw, "throw", 1) |
|
.inherit(iterator_prototype) |
|
.build(); |
|
|
|
context |
|
.standard_objects() |
|
.generator_object() |
|
.prototype |
|
.insert_property( |
|
"constructor", |
|
PropertyDescriptor::builder() |
|
.value(generator_function_prototype) |
|
.writable(false) |
|
.enumerable(false) |
|
.configurable(true), |
|
); |
|
|
|
obj.into() |
|
} |
|
} |
|
|
|
impl Generator { |
|
pub(crate) const LENGTH: usize = 0; |
|
|
|
#[allow(clippy::unnecessary_wraps)] |
|
pub(crate) fn constructor( |
|
_: &JsValue, |
|
_: &[JsValue], |
|
context: &mut Context, |
|
) -> JsResult<JsValue> { |
|
let prototype = context.standard_objects().generator_object().prototype(); |
|
|
|
let this = JsObject::from_proto_and_data( |
|
prototype, |
|
ObjectData::generator(Self { |
|
state: GeneratorState::Undefined, |
|
context: None, |
|
}), |
|
); |
|
|
|
Ok(this.into()) |
|
} |
|
|
|
/// `Generator.prototype.next ( value )` |
|
/// |
|
/// The `next()` method returns an object with two properties done and value. |
|
/// You can also provide a parameter to the next method to send a value to the generator. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.next |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next |
|
pub(crate) fn next( |
|
this: &JsValue, |
|
args: &[JsValue], |
|
context: &mut Context, |
|
) -> JsResult<JsValue> { |
|
// 1. Return ? GeneratorResume(this value, value, empty). |
|
match this.as_object() { |
|
Some(obj) if obj.is_generator() => { |
|
Self::generator_resume(obj, args.get_or_undefined(0), context) |
|
} |
|
_ => context.throw_type_error("Generator.prototype.next called on non generator"), |
|
} |
|
} |
|
|
|
/// `Generator.prototype.return ( value )` |
|
/// |
|
/// The `return()` method returns the given value and finishes the generator. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.return |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return |
|
pub(crate) fn r#return( |
|
this: &JsValue, |
|
args: &[JsValue], |
|
context: &mut Context, |
|
) -> JsResult<JsValue> { |
|
// 1. Let g be the this value. |
|
// 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. |
|
// 3. Return ? GeneratorResumeAbrupt(g, C, empty). |
|
Self::generator_resume_abrupt(this, Ok(args.get_or_undefined(0).clone()), context) |
|
} |
|
|
|
/// `Generator.prototype.throw ( exception )` |
|
/// |
|
/// The `throw()` method resumes the execution of a generator by throwing an error into it |
|
/// and returns an object with two properties done and value. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.throw |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw |
|
pub(crate) fn throw( |
|
this: &JsValue, |
|
args: &[JsValue], |
|
context: &mut Context, |
|
) -> JsResult<JsValue> { |
|
// 1. Let g be the this value. |
|
// 2. Let C be ThrowCompletion(exception). |
|
// 3. Return ? GeneratorResumeAbrupt(g, C, empty). |
|
Self::generator_resume_abrupt(this, Err(args.get_or_undefined(0).clone()), context) |
|
} |
|
|
|
/// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )` |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-generatorresume |
|
pub(crate) fn generator_resume( |
|
generator_obj: &JsObject, |
|
value: &JsValue, |
|
context: &mut Context, |
|
) -> JsResult<JsValue> { |
|
// 1. Let state be ? GeneratorValidate(generator, generatorBrand). |
|
let mut generator_obj_mut = generator_obj.borrow_mut(); |
|
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| { |
|
context.construct_type_error("generator resumed on non generator object") |
|
})?; |
|
let state = generator.state; |
|
|
|
if state == GeneratorState::Executing { |
|
return Err(context.construct_type_error("Generator should not be executing")); |
|
} |
|
|
|
// 2. If state is completed, return CreateIterResultObject(undefined, true). |
|
if state == GeneratorState::Completed { |
|
return Ok(create_iter_result_object( |
|
JsValue::undefined(), |
|
true, |
|
context, |
|
)); |
|
} |
|
|
|
// 3. Assert: state is either suspendedStart or suspendedYield. |
|
assert!(matches!( |
|
state, |
|
GeneratorState::SuspendedStart | GeneratorState::SuspendedYield |
|
)); |
|
|
|
// 4. Let genContext be generator.[[GeneratorContext]]. |
|
// 5. Let methodContext be the running execution context. |
|
// 6. Suspend methodContext. |
|
// 7. Set generator.[[GeneratorState]] to executing. |
|
generator.state = GeneratorState::Executing; |
|
let first_execution = matches!(state, GeneratorState::SuspendedStart); |
|
|
|
let generator_context_cell = generator |
|
.context |
|
.take() |
|
.expect("generator context cannot be empty here"); |
|
let mut generator_context = generator_context_cell.borrow_mut(); |
|
drop(generator_obj_mut); |
|
|
|
std::mem::swap( |
|
&mut context.realm.environments, |
|
&mut generator_context.environments, |
|
); |
|
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); |
|
context.vm.push_frame(generator_context.call_frame.clone()); |
|
if !first_execution { |
|
context.vm.push(value); |
|
} |
|
|
|
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal; |
|
|
|
let result = context.run(); |
|
|
|
generator_context.call_frame = *context |
|
.vm |
|
.pop_frame() |
|
.expect("generator call frame must exist"); |
|
std::mem::swap( |
|
&mut context.realm.environments, |
|
&mut generator_context.environments, |
|
); |
|
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); |
|
|
|
let mut generator_obj_mut = generator_obj.borrow_mut(); |
|
let generator = generator_obj_mut |
|
.as_generator_mut() |
|
.expect("already checked this object type"); |
|
|
|
match result { |
|
Ok((value, ReturnType::Yield)) => { |
|
generator.state = GeneratorState::SuspendedYield; |
|
drop(generator_context); |
|
generator.context = Some(generator_context_cell); |
|
Ok(create_iter_result_object(value, false, context)) |
|
} |
|
Ok((value, _)) => { |
|
generator.state = GeneratorState::Completed; |
|
Ok(create_iter_result_object(value, true, context)) |
|
} |
|
Err(value) => { |
|
generator.state = GeneratorState::Completed; |
|
Err(value) |
|
} |
|
} |
|
|
|
// 8. Push genContext onto the execution context stack; genContext is now the running execution context. |
|
// 9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it. Let result be the value returned by the resumed computation. |
|
// 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. |
|
// 11. Return Completion(result). |
|
} |
|
|
|
/// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )` |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt |
|
pub(crate) fn generator_resume_abrupt( |
|
this: &JsValue, |
|
abrupt_completion: JsResult<JsValue>, |
|
context: &mut Context, |
|
) -> JsResult<JsValue> { |
|
// 1. Let state be ? GeneratorValidate(generator, generatorBrand). |
|
let generator_obj = this.as_object().ok_or_else(|| { |
|
context.construct_type_error("generator resumed on non generator object") |
|
})?; |
|
let mut generator_obj_mut = generator_obj.borrow_mut(); |
|
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| { |
|
context.construct_type_error("generator resumed on non generator object") |
|
})?; |
|
let mut state = generator.state; |
|
|
|
if state == GeneratorState::Executing { |
|
return Err(context.construct_type_error("Generator should not be executing")); |
|
} |
|
|
|
// 2. If state is suspendedStart, then |
|
if state == GeneratorState::SuspendedStart { |
|
// a. Set generator.[[GeneratorState]] to completed. |
|
generator.state = GeneratorState::Completed; |
|
// b. Once a generator enters the completed state it never leaves it and its associated execution context is never resumed. Any execution state associated with generator can be discarded at this point. |
|
generator.context = None; |
|
// c. Set state to completed. |
|
state = GeneratorState::Completed; |
|
} |
|
|
|
// 3. If state is completed, then |
|
if state == GeneratorState::Completed { |
|
// a. If abruptCompletion.[[Type]] is return, then |
|
if let Ok(value) = abrupt_completion { |
|
// i. Return CreateIterResultObject(abruptCompletion.[[Value]], true). |
|
return Ok(create_iter_result_object(value, true, context)); |
|
} |
|
// b. Return Completion(abruptCompletion). |
|
return abrupt_completion; |
|
} |
|
|
|
// 4. Assert: state is suspendedYield. |
|
// 5. Let genContext be generator.[[GeneratorContext]]. |
|
// 6. Let methodContext be the running execution context. |
|
// 7. Suspend methodContext. |
|
// 8. Set generator.[[GeneratorState]] to executing. |
|
// 9. Push genContext onto the execution context stack; genContext is now the running execution context. |
|
// 10. Resume the suspended evaluation of genContext using abruptCompletion as the result of the operation that suspended it. Let result be the completion record returned by the resumed computation. |
|
// 11. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. |
|
// 12. Return Completion(result). |
|
let generator_context_cell = generator |
|
.context |
|
.take() |
|
.expect("generator context cannot be empty here"); |
|
let mut generator_context = generator_context_cell.borrow_mut(); |
|
|
|
generator.state = GeneratorState::Executing; |
|
drop(generator_obj_mut); |
|
|
|
std::mem::swap( |
|
&mut context.realm.environments, |
|
&mut generator_context.environments, |
|
); |
|
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); |
|
context.vm.push_frame(generator_context.call_frame.clone()); |
|
|
|
let result = match abrupt_completion { |
|
Ok(value) => { |
|
context.vm.push(value); |
|
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return; |
|
context.run() |
|
} |
|
Err(value) => { |
|
context.vm.push(value); |
|
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw; |
|
context.run() |
|
} |
|
}; |
|
generator_context.call_frame = *context |
|
.vm |
|
.pop_frame() |
|
.expect("generator call frame must exist"); |
|
std::mem::swap( |
|
&mut context.realm.environments, |
|
&mut generator_context.environments, |
|
); |
|
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); |
|
|
|
let mut generator_obj_mut = generator_obj.borrow_mut(); |
|
let generator = generator_obj_mut |
|
.as_generator_mut() |
|
.expect("already checked this object type"); |
|
|
|
match result { |
|
Ok((value, ReturnType::Yield)) => { |
|
generator.state = GeneratorState::SuspendedYield; |
|
drop(generator_context); |
|
generator.context = Some(generator_context_cell); |
|
Ok(create_iter_result_object(value, false, context)) |
|
} |
|
Ok((value, _)) => { |
|
generator.state = GeneratorState::Completed; |
|
Ok(create_iter_result_object(value, true, context)) |
|
} |
|
Err(value) => { |
|
generator.state = GeneratorState::Completed; |
|
Err(value) |
|
} |
|
} |
|
} |
|
}
|
|
|