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