Skip to main content

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}