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}