boa_engine/vm/opcode/await/
mod.rs

1use std::cell::Cell;
2
3use boa_gc::Gc;
4
5use crate::{
6    builtins::{
7        async_generator::AsyncGenerator, generator::GeneratorContext, promise::PromiseCapability,
8        Promise,
9    },
10    js_string,
11    native_function::NativeFunction,
12    object::FunctionObjectBuilder,
13    vm::{opcode::Operation, CompletionType, GeneratorResumeKind},
14    Context, JsArgs, JsResult, JsValue,
15};
16
17/// `Await` implements the Opcode Operation for `Opcode::Await`
18///
19/// Operation:
20///  - Stops the current Async function and schedules it to resume later.
21#[derive(Debug, Clone, Copy)]
22pub(crate) struct Await;
23
24impl Operation for Await {
25    const NAME: &'static str = "Await";
26    const INSTRUCTION: &'static str = "INST - Await";
27    const COST: u8 = 5;
28
29    fn execute(context: &mut Context) -> JsResult<CompletionType> {
30        let value = context.vm.pop();
31
32        // 2. Let promise be ? PromiseResolve(%Promise%, value).
33        let promise = Promise::promise_resolve(
34            &context.intrinsics().constructors().promise().constructor(),
35            value,
36            context,
37        )?;
38
39        let return_value = context
40            .vm
41            .frame()
42            .promise_capability(&context.vm.stack)
43            .as_ref()
44            .map(PromiseCapability::promise)
45            .cloned()
46            .map(JsValue::from)
47            .unwrap_or_default();
48
49        let gen = GeneratorContext::from_current(context);
50
51        let captures = Gc::new(Cell::new(Some(gen)));
52
53        // 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called:
54        // 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
55        let on_fulfilled = FunctionObjectBuilder::new(
56            context.realm(),
57            NativeFunction::from_copy_closure_with_captures(
58                |_this, args, captures, context| {
59                    // a. Let prevContext be the running execution context.
60                    // b. Suspend prevContext.
61                    // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
62                    // d. Resume the suspended evaluation of asyncContext using NormalCompletion(value) as the result of the operation that suspended it.
63                    let mut gen = captures.take().expect("should only run once");
64
65                    // NOTE: We need to get the object before resuming, since it could clear the stack.
66                    let async_generator = gen.async_generator_object();
67
68                    gen.resume(
69                        Some(args.get_or_undefined(0).clone()),
70                        GeneratorResumeKind::Normal,
71                        context,
72                    );
73
74                    if let Some(async_generator) = async_generator {
75                        async_generator
76                            .downcast_mut::<AsyncGenerator>()
77                            .expect("must be async generator")
78                            .context = Some(gen);
79                    }
80
81                    // e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context.
82                    // f. Return undefined.
83                    Ok(JsValue::undefined())
84                },
85                captures.clone(),
86            ),
87        )
88        .name(js_string!())
89        .length(1)
90        .build();
91
92        // 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the following steps when called:
93        // 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
94        let on_rejected = FunctionObjectBuilder::new(
95            context.realm(),
96            NativeFunction::from_copy_closure_with_captures(
97                |_this, args, captures, context| {
98                    // a. Let prevContext be the running execution context.
99                    // b. Suspend prevContext.
100                    // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
101                    // d. Resume the suspended evaluation of asyncContext using ThrowCompletion(reason) as the result of the operation that suspended it.
102                    // e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context.
103                    // f. Return undefined.
104                    let mut gen = captures.take().expect("should only run once");
105
106                    // NOTE: We need to get the object before resuming, since it could clear the stack.
107                    let async_generator = gen.async_generator_object();
108
109                    gen.resume(
110                        Some(args.get_or_undefined(0).clone()),
111                        GeneratorResumeKind::Throw,
112                        context,
113                    );
114
115                    if let Some(async_generator) = async_generator {
116                        async_generator
117                            .downcast_mut::<AsyncGenerator>()
118                            .expect("must be async generator")
119                            .context = Some(gen);
120                    }
121
122                    Ok(JsValue::undefined())
123                },
124                captures,
125            ),
126        )
127        .name(js_string!())
128        .length(1)
129        .build();
130
131        // 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected).
132        Promise::perform_promise_then(
133            &promise,
134            Some(on_fulfilled),
135            Some(on_rejected),
136            None,
137            context,
138        );
139
140        context.vm.set_return_value(return_value);
141        Ok(CompletionType::Yield)
142    }
143}
144
145/// `CreatePromiseCapability` implements the Opcode Operation for `Opcode::CreatePromiseCapability`
146///
147/// Operation:
148///  - Create a promise capacity for an async function, if not already set.
149#[derive(Debug, Clone, Copy)]
150pub(crate) struct CreatePromiseCapability;
151
152impl Operation for CreatePromiseCapability {
153    const NAME: &'static str = "CreatePromiseCapability";
154    const INSTRUCTION: &'static str = "INST - CreatePromiseCapability";
155    const COST: u8 = 8;
156
157    fn execute(context: &mut Context) -> JsResult<CompletionType> {
158        if context
159            .vm
160            .frame()
161            .promise_capability(&context.vm.stack)
162            .is_some()
163        {
164            return Ok(CompletionType::Normal);
165        }
166
167        let promise_capability = PromiseCapability::new(
168            &context.intrinsics().constructors().promise().constructor(),
169            context,
170        )
171        .expect("cannot fail per spec");
172
173        context
174            .vm
175            .frame
176            .set_promise_capability(&mut context.vm.stack, Some(&promise_capability));
177        Ok(CompletionType::Normal)
178    }
179}
180
181/// `CompletePromiseCapability` implements the Opcode Operation for `Opcode::CompletePromiseCapability`
182///
183/// Operation:
184///  - Resolves or rejects the promise capability, depending if the pending exception is set.
185#[derive(Debug, Clone, Copy)]
186pub(crate) struct CompletePromiseCapability;
187
188impl Operation for CompletePromiseCapability {
189    const NAME: &'static str = "CompletePromiseCapability";
190    const INSTRUCTION: &'static str = "INST - CompletePromiseCapability";
191    const COST: u8 = 8;
192
193    fn execute(context: &mut Context) -> JsResult<CompletionType> {
194        // If the current executing function is an async function we have to resolve/reject it's promise at the end.
195        // The relevant spec section is 3. in [AsyncBlockStart](https://tc39.es/ecma262/#sec-asyncblockstart).
196        let Some(promise_capability) = context.vm.frame().promise_capability(&context.vm.stack)
197        else {
198            return if context.vm.pending_exception.is_some() {
199                Ok(CompletionType::Throw)
200            } else {
201                Ok(CompletionType::Normal)
202            };
203        };
204
205        if let Some(error) = context.vm.pending_exception.take() {
206            promise_capability
207                .reject()
208                .call(&JsValue::undefined(), &[error.to_opaque(context)], context)
209                .expect("cannot fail per spec");
210        } else {
211            let return_value = context.vm.get_return_value();
212            promise_capability
213                .resolve()
214                .call(&JsValue::undefined(), &[return_value], context)
215                .expect("cannot fail per spec");
216        };
217
218        context
219            .vm
220            .set_return_value(promise_capability.promise().clone().into());
221
222        Ok(CompletionType::Normal)
223    }
224}