boa_engine/builtins/generator/mod.rs
1//! Boa's implementation of ECMAScript's global `Generator` object.
2//!
3//! A Generator is an instance of a generator function and conforms to both the Iterator and Iterable interfaces.
4//!
5//! More information:
6//! - [ECMAScript reference][spec]
7//! - [MDN documentation][mdn]
8//!
9//! [spec]: https://tc39.es/ecma262/#sec-generator-objects
10//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
11
12use crate::{
13 Context, JsArgs, JsData, JsError, JsExpect, JsResult, JsString,
14 builtins::iterable::create_iter_result_object,
15 context::intrinsics::Intrinsics,
16 error::JsNativeError,
17 error::PanicError,
18 js_string,
19 object::{CONSTRUCTOR, JsObject},
20 property::Attribute,
21 realm::Realm,
22 string::StaticJsStrings,
23 symbol::JsSymbol,
24 value::JsValue,
25 vm::{CallFrame, CallFrameFlags, CompletionRecord, GeneratorResumeKind, Stack},
26};
27use boa_gc::{Finalize, Trace, custom_trace};
28
29use super::{BuiltInBuilder, IntrinsicObject};
30
31/// Indicates the state of a generator.
32#[derive(Debug, Finalize)]
33pub(crate) enum GeneratorState {
34 SuspendedStart {
35 /// The `[[GeneratorContext]]` internal slot.
36 context: GeneratorContext,
37 },
38 SuspendedYield {
39 /// The `[[GeneratorContext]]` internal slot.
40 context: GeneratorContext,
41 },
42 Executing,
43 Completed,
44}
45
46// Need to manually implement, since `Trace` adds a `Drop` impl which disallows destructuring.
47unsafe impl Trace for GeneratorState {
48 custom_trace!(this, mark, {
49 match &this {
50 Self::SuspendedStart { context } | Self::SuspendedYield { context } => mark(context),
51 Self::Executing | Self::Completed => {}
52 }
53 });
54}
55
56/// Holds all information that a generator needs to continue it's execution.
57///
58/// All of the fields must be changed with those that are currently present in the
59/// context/vm before the generator execution starts/resumes and after it has ended/yielded.
60#[derive(Debug, Trace, Finalize)]
61pub(crate) struct GeneratorContext {
62 pub(crate) stack: Stack,
63 pub(crate) call_frame: Option<CallFrame>,
64}
65
66impl GeneratorContext {
67 /// Creates a new `GeneratorContext` from the current `Context` state.
68 pub(crate) fn from_current(context: &mut Context, async_generator: Option<JsObject>) -> Self {
69 let mut frame = context.vm.frame().clone();
70 frame.environments = context.vm.frame().environments.clone();
71 frame.realm = context.realm().clone();
72
73 // Split the stack at fp. The split-off portion starts at what was fp,
74 // so adjust rp and fp to be relative to the new base.
75 let mut stack = context.vm.stack.split_off_frame(&frame);
76 frame.rp -= frame.fp;
77 frame.fp = 0;
78
79 // NOTE: Since we get a pre-built call frame with stack, and we reuse them.
80 // So we don't need to push the registers in subsequent calls.
81 frame.flags |= CallFrameFlags::REGISTERS_ALREADY_PUSHED;
82
83 if let Some(async_generator) = async_generator {
84 stack.set_register(
85 &frame,
86 CallFrame::ASYNC_GENERATOR_OBJECT_REGISTER_INDEX,
87 async_generator.into(),
88 );
89 }
90
91 Self {
92 call_frame: Some(frame),
93 stack,
94 }
95 }
96
97 /// Resumes execution with `GeneratorContext` as the current execution context.
98 pub(crate) fn resume(
99 &mut self,
100 value: Option<JsValue>,
101 resume_kind: GeneratorResumeKind,
102 context: &mut Context,
103 ) -> CompletionRecord {
104 std::mem::swap(&mut context.vm.stack, &mut self.stack);
105 let Some(frame) = self.call_frame.take() else {
106 return CompletionRecord::Throw(PanicError::new("should have a call frame").into());
107 };
108 let fp = frame.fp;
109 let rp = frame.rp;
110 context.vm.push_frame(frame);
111
112 let frame = context.vm.frame_mut();
113 frame.fp = fp;
114 frame.rp = rp;
115 frame.set_exit_early(true);
116
117 if let Some(value) = value {
118 context.vm.stack.push(value);
119 }
120 context.vm.stack.push(resume_kind);
121
122 let result = context.run();
123
124 std::mem::swap(&mut context.vm.stack, &mut self.stack);
125 self.call_frame = context.vm.pop_frame();
126 assert!(self.call_frame.is_some());
127 result
128 }
129
130 /// Returns the async generator object, if the function that this [`GeneratorContext`] is from an async generator, [`None`] otherwise.
131 pub(crate) fn async_generator_object(&self) -> JsResult<Option<JsObject>> {
132 let Some(frame) = self.call_frame.as_ref() else {
133 return Ok(None);
134 };
135
136 if !frame.code_block().is_async_generator() {
137 return Ok(None);
138 }
139
140 let val = self
141 .stack
142 .get_register(frame, CallFrame::ASYNC_GENERATOR_OBJECT_REGISTER_INDEX)
143 .js_expect("registers must have an async generator object")?;
144
145 Ok(val.as_object())
146 }
147}
148
149/// The internal representation of a `Generator` object.
150#[derive(Debug, Finalize, Trace, JsData)]
151pub struct Generator {
152 /// The `[[GeneratorState]]` internal slot.
153 pub(crate) state: GeneratorState,
154}
155
156impl IntrinsicObject for Generator {
157 fn init(realm: &Realm) {
158 BuiltInBuilder::with_intrinsic::<Self>(realm)
159 .prototype(realm.intrinsics().constructors().iterator().prototype())
160 .static_method(Self::next, js_string!("next"), 1)
161 .static_method(Self::r#return, js_string!("return"), 1)
162 .static_method(Self::throw, js_string!("throw"), 1)
163 .static_property(
164 JsSymbol::to_string_tag(),
165 Self::NAME,
166 Attribute::CONFIGURABLE,
167 )
168 .static_property(
169 CONSTRUCTOR,
170 realm
171 .intrinsics()
172 .constructors()
173 .generator_function()
174 .prototype(),
175 Attribute::CONFIGURABLE,
176 )
177 .build();
178 }
179
180 fn get(intrinsics: &Intrinsics) -> JsObject {
181 intrinsics.objects().generator()
182 }
183}
184
185impl Generator {
186 const NAME: JsString = StaticJsStrings::GENERATOR;
187
188 /// `Generator.prototype.next ( value )`
189 ///
190 /// The `next()` method returns an object with two properties done and value.
191 /// You can also provide a parameter to the next method to send a value to the generator.
192 ///
193 /// More information:
194 /// - [ECMAScript reference][spec]
195 /// - [MDN documentation][mdn]
196 ///
197 /// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.next
198 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next
199 pub(crate) fn next(
200 this: &JsValue,
201 args: &[JsValue],
202 context: &mut Context,
203 ) -> JsResult<JsValue> {
204 // 1. Return ? GeneratorResume(this value, value, empty).
205 Self::generator_resume(this, args.get_or_undefined(0).clone(), context)
206 }
207
208 /// `Generator.prototype.return ( value )`
209 ///
210 /// The `return()` method returns the given value and finishes the generator.
211 ///
212 /// More information:
213 /// - [ECMAScript reference][spec]
214 /// - [MDN documentation][mdn]
215 ///
216 /// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.return
217 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return
218 pub(crate) fn r#return(
219 this: &JsValue,
220 args: &[JsValue],
221 context: &mut Context,
222 ) -> JsResult<JsValue> {
223 // 1. Let g be the this value.
224 // 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }.
225 // 3. Return ? GeneratorResumeAbrupt(g, C, empty).
226 Self::generator_resume_abrupt(this, Ok(args.get_or_undefined(0).clone()), context)
227 }
228
229 /// `Generator.prototype.throw ( exception )`
230 ///
231 /// The `throw()` method resumes the execution of a generator by throwing an error into it
232 /// and returns an object with two properties done and value.
233 ///
234 /// More information:
235 /// - [ECMAScript reference][spec]
236 /// - [MDN documentation][mdn]
237 ///
238 /// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.throw
239 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw
240 pub(crate) fn throw(
241 this: &JsValue,
242 args: &[JsValue],
243 context: &mut Context,
244 ) -> JsResult<JsValue> {
245 // 1. Let g be the this value.
246 // 2. Let C be ThrowCompletion(exception).
247 // 3. Return ? GeneratorResumeAbrupt(g, C, empty).
248 Self::generator_resume_abrupt(
249 this,
250 Err(JsError::from_opaque(args.get_or_undefined(0).clone())),
251 context,
252 )
253 }
254
255 /// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )`
256 ///
257 /// More information:
258 /// - [ECMAScript reference][spec]
259 ///
260 /// [spec]: https://tc39.es/ecma262/#sec-generatorresume
261 pub(crate) fn generator_resume(
262 r#gen: &JsValue,
263 value: JsValue,
264 context: &mut Context,
265 ) -> JsResult<JsValue> {
266 // 1. Let state be ? GeneratorValidate(generator, generatorBrand).
267 let Some(generator_obj) = r#gen.as_object() else {
268 return Err(JsNativeError::typ()
269 .with_message("Generator method called on non generator")
270 .into());
271 };
272 let mut r#gen = generator_obj.downcast_mut::<Self>().ok_or_else(|| {
273 JsNativeError::typ().with_message("generator resumed on non generator object")
274 })?;
275
276 // 4. Let genContext be generator.[[GeneratorContext]].
277 // 5. Let methodContext be the running execution context.
278 // 6. Suspend methodContext.
279 // 7. Set generator.[[GeneratorState]] to executing.
280 let (mut generator_context, first_execution) =
281 match std::mem::replace(&mut r#gen.state, GeneratorState::Executing) {
282 GeneratorState::Executing => {
283 return Err(JsNativeError::typ()
284 .with_message("Generator should not be executing")
285 .into());
286 }
287 // 2. If state is completed, return CreateIterResultObject(undefined, true).
288 GeneratorState::Completed => {
289 r#gen.state = GeneratorState::Completed;
290 return Ok(create_iter_result_object(
291 JsValue::undefined(),
292 true,
293 context,
294 ));
295 }
296 // 3. Assert: state is either suspendedStart or suspendedYield.
297 GeneratorState::SuspendedStart { context } => (context, true),
298 GeneratorState::SuspendedYield { context } => (context, false),
299 };
300
301 drop(r#gen);
302
303 let record = generator_context.resume(
304 (!first_execution).then_some(value),
305 GeneratorResumeKind::Normal,
306 context,
307 );
308
309 let mut r#gen = generator_obj
310 .downcast_mut::<Self>()
311 .js_expect("already checked this object type")?;
312
313 // 8. Push genContext onto the execution context stack; genContext is now the running execution context.
314 // 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.
315 // 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context.
316 // 11. Return Completion(result).
317 match record {
318 CompletionRecord::Normal(value) => {
319 r#gen.state = GeneratorState::SuspendedYield {
320 context: generator_context,
321 };
322 Ok(value)
323 }
324 CompletionRecord::Return(value) => {
325 r#gen.state = GeneratorState::Completed;
326 Ok(create_iter_result_object(value, true, context))
327 }
328 CompletionRecord::Throw(err) => {
329 r#gen.state = GeneratorState::Completed;
330 Err(err)
331 }
332 }
333 }
334
335 /// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )`
336 ///
337 /// More information:
338 /// - [ECMAScript reference][spec]
339 ///
340 /// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt
341 pub(crate) fn generator_resume_abrupt(
342 r#gen: &JsValue,
343 abrupt_completion: JsResult<JsValue>,
344 context: &mut Context,
345 ) -> JsResult<JsValue> {
346 // 1. Let state be ? GeneratorValidate(generator, generatorBrand).
347 let Some(generator_obj) = r#gen.as_object() else {
348 return Err(JsNativeError::typ()
349 .with_message("Generator method called on non generator")
350 .into());
351 };
352 let mut r#gen = generator_obj.downcast_mut::<Self>().ok_or_else(|| {
353 JsNativeError::typ().with_message("generator resumed on non generator object")
354 })?;
355
356 // 4. Assert: state is suspendedYield.
357 // 5. Let genContext be generator.[[GeneratorContext]].
358 // 6. Let methodContext be the running execution context.
359 // 7. Suspend methodContext.
360 // 8. Set generator.[[GeneratorState]] to executing.
361 let mut generator_context =
362 match std::mem::replace(&mut r#gen.state, GeneratorState::Executing) {
363 GeneratorState::Executing => {
364 return Err(JsNativeError::typ()
365 .with_message("Generator should not be executing")
366 .into());
367 }
368 // 2. If state is suspendedStart, then
369 // 3. If state is completed, then
370 GeneratorState::SuspendedStart { .. } | GeneratorState::Completed => {
371 // a. Set generator.[[GeneratorState]] to completed.
372 r#gen.state = GeneratorState::Completed;
373
374 // b. Once a generator enters the completed state it never leaves it and its
375 // associated execution context is never resumed. Any execution state associated
376 // with generator can be discarded at this point.
377
378 // a. If abruptCompletion.[[Type]] is return, then
379 if let Ok(value) = abrupt_completion {
380 // i. Return CreateIterResultObject(abruptCompletion.[[Value]], true).
381 let value = create_iter_result_object(value, true, context);
382 return Ok(value);
383 }
384
385 // b. Return Completion(abruptCompletion).
386 return abrupt_completion;
387 }
388 GeneratorState::SuspendedYield { context } => context,
389 };
390
391 // 9. Push genContext onto the execution context stack; genContext is now the running execution context.
392 // 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.
393 // 11. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context.
394 // 12. Return Completion(result).
395 drop(r#gen);
396
397 let (value, resume_kind) = match abrupt_completion {
398 Ok(value) => (value, GeneratorResumeKind::Return),
399 Err(err) => (err.into_opaque(context)?, GeneratorResumeKind::Throw),
400 };
401
402 let record = generator_context.resume(Some(value), resume_kind, context);
403
404 let mut r#gen = generator_obj.downcast_mut::<Self>().ok_or_else(|| {
405 JsNativeError::typ().with_message("generator resumed on non generator object")
406 })?;
407
408 match record {
409 CompletionRecord::Normal(value) => {
410 r#gen.state = GeneratorState::SuspendedYield {
411 context: generator_context,
412 };
413 Ok(value)
414 }
415 CompletionRecord::Return(value) => {
416 r#gen.state = GeneratorState::Completed;
417 Ok(create_iter_result_object(value, true, context))
418 }
419 CompletionRecord::Throw(err) => {
420 r#gen.state = GeneratorState::Completed;
421 Err(err)
422 }
423 }
424 }
425}