-
Notifications
You must be signed in to change notification settings - Fork 76
fix(vm): restore async-generator invariants and PromiseResolve abrupt handling (#706, #707, #708, #709) #950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
57f538d
6916636
879b032
cf52ab7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,25 +37,25 @@ impl AsyncGenerator<'_> { | |
| self.get(agent).executable.unwrap().bind(gc) | ||
| } | ||
|
|
||
| /// Returns true if the state of the AsyncGenerator is DRAINING-QUEUE or | ||
| /// EXECUTING. | ||
| /// | ||
| /// > NOTE: In our implementation, EXECUTING is split into an extra | ||
| /// > EXECUTING-AWAIT state. This also checks for that. | ||
| pub(crate) fn is_active(self, agent: &Agent) -> bool { | ||
| pub(crate) fn is_draining_queue(self, agent: &Agent) -> bool { | ||
| self.get(agent) | ||
| .async_generator_state | ||
| .as_ref() | ||
| .unwrap() | ||
| .is_active() | ||
| .is_draining_queue() | ||
| } | ||
|
|
||
| pub(crate) fn is_draining_queue(self, agent: &Agent) -> bool { | ||
| /// Returns true if the state of the AsyncGenerator is DRAINING-QUEUE or | ||
| /// EXECUTING. | ||
| /// | ||
| /// NOTE: In our implementation, EXECUTING is split into an extra | ||
| /// EXECUTING-AWAIT state. This also checks for that. | ||
| pub(crate) fn is_active(self, agent: &Agent) -> bool { | ||
| self.get(agent) | ||
| .async_generator_state | ||
| .as_ref() | ||
| .unwrap() | ||
| .is_draining_queue() | ||
| .is_active() | ||
| } | ||
|
|
||
| pub(crate) fn is_executing(self, agent: &Agent) -> bool { | ||
|
|
@@ -200,17 +200,18 @@ impl AsyncGenerator<'_> { | |
| gc: NoGcScope<'gc, '_>, | ||
| ) -> (SuspendedVm, ExecutionContext, Executable<'gc>) { | ||
| let async_generator_state = &mut self.get_mut(agent).async_generator_state; | ||
| let (vm, execution_context, queue) = match async_generator_state.take() { | ||
| Some(AsyncGeneratorState::SuspendedStart { | ||
| let state = async_generator_state.take().unwrap(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Meaningless change. |
||
| let (vm, execution_context, queue) = match state { | ||
| AsyncGeneratorState::SuspendedStart { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: I'm not sure if this can happen... If it can, then this is a really nice catch and explains a lot about the failures. I'd like to see only these changes in a separate PR and see the effect that has. |
||
| vm, | ||
| execution_context, | ||
| queue, | ||
| }) => (vm, execution_context, queue), | ||
| Some(AsyncGeneratorState::SuspendedYield { | ||
| } => (vm, execution_context, queue), | ||
| AsyncGeneratorState::SuspendedYield { | ||
| vm, | ||
| execution_context, | ||
| queue, | ||
| }) => (vm, execution_context, queue), | ||
| } => (vm, execution_context, queue), | ||
| _ => unreachable!(), | ||
| }; | ||
| async_generator_state.replace(AsyncGeneratorState::Executing(queue)); | ||
|
|
@@ -258,6 +259,11 @@ impl AsyncGenerator<'_> { | |
| // 1. Assert: generator.[[AsyncGeneratorState]] is either suspended-start or suspended-yield. | ||
| let state = self.get_mut(agent).async_generator_state.take().unwrap(); | ||
| let (vm, execution_context, queue, kind) = match state { | ||
| AsyncGeneratorState::SuspendedStart { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Same as above. |
||
| vm, | ||
| execution_context, | ||
| queue, | ||
| } => (vm, execution_context, queue, AsyncGeneratorAwaitKind::Await), | ||
| AsyncGeneratorState::SuspendedYield { | ||
| vm, | ||
| execution_context, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,8 @@ | |
| use crate::{ | ||
| ecmascript::{ | ||
| Agent, ECMAScriptFunction, ExceptionType, JsError, JsResult, Promise, PromiseCapability, | ||
| PromiseReactionHandler, Realm, Value, create_iter_result_object, inner_promise_then, | ||
| unwrap_try, | ||
| PromiseReactionHandler, PromiseReactionType, Realm, Value, create_iter_result_object, | ||
| inner_promise_then, unwrap_try, | ||
| }, | ||
| engine::{Bindable, ExecutionResult, GcScope, NoGcScope, Scopable, Scoped, SuspendedVm}, | ||
| heap::ArenaAccessMut, | ||
|
|
@@ -257,14 +257,19 @@ fn async_generator_perform_await( | |
| let execution_context = agent.pop_execution_context().unwrap(); | ||
| let generator = scoped_generator.get(agent).bind(gc.nogc()); | ||
| generator.transition_to_awaiting(agent, vm, kind, execution_context); | ||
| let generator = generator.unbind(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: |
||
| // 8. Remove asyncContext from the execution context stack and | ||
| // restore the execution context that is at the top of the | ||
| // execution context stack as the running execution context. | ||
| let handler = PromiseReactionHandler::AsyncGenerator(generator.unbind()); | ||
| let handler = PromiseReactionHandler::AsyncGenerator(generator); | ||
| // 2. Let promise be ? PromiseResolve(%Promise%, value). | ||
| let promise = Promise::resolve(agent, awaited_value, gc.reborrow()) | ||
| .unbind() | ||
| .bind(gc.nogc()); | ||
| let promise = match Promise::try_resolve(agent, awaited_value, gc.reborrow()) { | ||
| Ok(promise) => promise.unbind().bind(gc.nogc()), | ||
| Err(err) => { | ||
| generator.resume_await(agent, PromiseReactionType::Reject, err.value().unbind(), gc); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: This seems suspect; I am not sure if this is correct. |
||
| return; | ||
| } | ||
| }; | ||
|
|
||
| // 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected). | ||
| inner_promise_then(agent, promise, handler, handler, None, gc.nogc()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: handler is now use-after-free because |
||
|
|
@@ -404,16 +409,34 @@ pub(crate) fn async_generator_await_return( | |
| let AsyncGeneratorRequestCompletion::Return(value) = completion else { | ||
| unreachable!() | ||
| }; | ||
| let return_value = value.unbind().bind(gc.nogc()); | ||
| // 7. Let promiseCompletion be Completion(PromiseResolve(%Promise%, completion.[[Value]])). | ||
| // 8. If promiseCompletion is an abrupt completion, then | ||
| // a. Perform AsyncGeneratorCompleteStep(generator, promiseCompletion, true). | ||
| // b. Perform AsyncGeneratorDrainQueue(generator). | ||
| // c. Return unused. | ||
| let promise_completion = Promise::try_resolve(agent, return_value.unbind(), gc.reborrow()) | ||
| .map(|promise| promise.unbind()) | ||
| .map_err(|err| err.unbind()); | ||
| let promise = match promise_completion { | ||
| // 8. If promiseCompletion is an abrupt completion, then | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. praise: Based on the comment at least, this indeed seems like a missing handling. Nice catch. |
||
| // a. Perform AsyncGeneratorCompleteStep(generator, promiseCompletion, true). | ||
| // b. Perform AsyncGeneratorDrainQueue(generator). | ||
| // c. Return unused. | ||
| Err(err) => { | ||
| let generator = scoped_generator.get(agent).bind(gc.nogc()); | ||
| async_generator_complete_step( | ||
| agent, | ||
| generator.unbind(), | ||
| AsyncGeneratorRequestCompletion::Err(err.unbind()), | ||
| true, | ||
| None, | ||
| gc.nogc(), | ||
| ); | ||
| async_generator_drain_queue(agent, scoped_generator, gc); | ||
| return; | ||
| } | ||
| Ok(promise) => promise.unbind().bind(gc.nogc()), | ||
| }; | ||
|
|
||
| // 9. Assert: promiseCompletion is a normal completion. | ||
| // 10. Let promise be promiseCompletion.[[Value]]. | ||
| let promise = Promise::resolve(agent, value.unbind(), gc.reborrow()) | ||
| .unbind() | ||
| .bind(gc.nogc()); | ||
| // 11. ... onFulfilled ... | ||
| // 12. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). | ||
| // 13. ... onRejected ... | ||
|
|
@@ -498,7 +521,9 @@ fn async_generator_drain_queue( | |
| let data = generator.get_mut(agent); | ||
| // Assert: generator.[[AsyncGeneratorState]] is draining-queue. | ||
| // 2. Let queue be generator.[[AsyncGeneratorQueue]]. | ||
| let Some(AsyncGeneratorState::DrainingQueue(queue)) = &mut data.async_generator_state else { | ||
| let AsyncGeneratorState::DrainingQueue(queue) = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Meaningless change.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: This is meaningless; unwrap the Option or unreachable the None – it's the same thing. |
||
| &mut data.async_generator_state.as_mut().unwrap() | ||
| else { | ||
| unreachable!() | ||
| }; | ||
| // 3. If queue is empty, then | ||
|
|
@@ -536,7 +561,8 @@ fn async_generator_drain_queue( | |
| async_generator_complete_step(agent, generator, completion, true, None, gc.nogc()); | ||
| let data = generator.get_mut(agent); | ||
| // iii. If queue is empty, then | ||
| let Some(AsyncGeneratorState::DrainingQueue(queue)) = &mut data.async_generator_state | ||
| let AsyncGeneratorState::DrainingQueue(queue) = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: This is meaningless; unwrap the Option or unreachable the None – it's the same thing. |
||
| &mut data.async_generator_state.as_mut().unwrap() | ||
| else { | ||
| unreachable!() | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,10 @@ use crate::{ | |
| script_var_scoped_declarations, to_int32, to_int32_number, to_number, to_number_primitive, | ||
| to_string, | ||
| }, | ||
| engine::{Bindable, Executable, GcScope, NoGcScope, Scopable, Vm, string_literal_to_wtf8}, | ||
| engine::{ | ||
| Bindable, Executable, ExecutionResult, GcScope, NoGcScope, Scopable, Vm, | ||
| string_literal_to_wtf8, | ||
| }, | ||
| heap::{ArenaAccess, HeapIndexHandle, IntrinsicFunctionIndexes}, | ||
| ndt, | ||
| }; | ||
|
|
@@ -391,7 +394,17 @@ pub(crate) fn perform_eval<'gc>( | |
| // a. Set result to Completion(Evaluation of body). | ||
| // 30. If result is a normal completion and result.[[Value]] is empty, then | ||
| // a. Set result to NormalCompletion(undefined). | ||
| let result = Vm::execute(agent, exe.clone(), None, gc).into_js_result(); | ||
| let result = match Vm::execute(agent, exe.clone(), None, gc.reborrow()) { | ||
| ExecutionResult::Return(value) => Ok(value.unbind()), | ||
| ExecutionResult::Throw(err) => Err(err.unbind()), | ||
| ExecutionResult::Await { .. } | ExecutionResult::Yield { .. } => Err(agent | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: This should be impossible to happen, we should get an early SyntaxError from our parser. This also does not seem to have anything to do with the async-generator work. |
||
| .throw_exception_with_static_message( | ||
| ExceptionType::SyntaxError, | ||
| "Invalid eval source text: unexpected await or yield in script.", | ||
| gc.nogc(), | ||
| ) | ||
| .unbind()), | ||
| }; | ||
| // SAFETY: No one can access the bytecode anymore. | ||
| unsafe { exe.take(agent).try_drop(agent) }; | ||
| result | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,8 +8,8 @@ pub(crate) use data::*; | |
|
|
||
| use crate::{ | ||
| ecmascript::{ | ||
| Agent, InternalMethods, InternalSlots, JsError, JsResult, OrdinaryObject, | ||
| PromiseCapability, ProtoIntrinsics, Value, object_handle, | ||
| Agent, BUILTIN_STRING_MEMORY, InternalMethods, InternalSlots, JsError, JsResult, | ||
| OrdinaryObject, PromiseCapability, ProtoIntrinsics, Value, get, object_handle, | ||
| }, | ||
| engine::{Bindable, GcScope, NoGcScope, Scopable}, | ||
| heap::{ | ||
|
|
@@ -25,6 +25,40 @@ object_handle!(Promise); | |
| arena_vec_access!(Promise, 'a, PromiseHeapData, promises); | ||
|
|
||
| impl<'a> Promise<'a> { | ||
| ///### [27.2.4.7.1 PromiseResolve ( C, x )](https://tc39.es/ecma262/#sec-promise-resolve) | ||
| /// | ||
| /// This variant implements abrupt-completion behavior for internal callers | ||
| /// that must handle `? PromiseResolve(%Promise%, x)`. | ||
| pub fn try_resolve(agent: &mut Agent, x: Value, mut gc: GcScope<'a, '_>) -> JsResult<'a, Self> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: |
||
| // 1. If IsPromise(x) is true, then | ||
| if let Value::Promise(promise) = x { | ||
| // a. Let xConstructor be ? Get(x, "constructor"). | ||
| let x_constructor = match get( | ||
| agent, | ||
| promise, | ||
| BUILTIN_STRING_MEMORY.constructor.into(), | ||
| gc.reborrow(), | ||
| ) { | ||
| Ok(value) => value.unbind().bind(gc.nogc()), | ||
| Err(err) => return Err(err.unbind().bind(gc.into_nogc())), | ||
| }; | ||
| // b. If SameValue(xConstructor, C) is true, return x. | ||
| // NOTE: Ignoring subclasses. | ||
| if x_constructor == agent.current_realm_record().intrinsics().promise().into() { | ||
| return Ok(promise.bind(gc.into_nogc())); | ||
| } | ||
| } | ||
| // 2. Let promiseCapability be ? NewPromiseCapability(C). | ||
| let promise_capability = PromiseCapability::new(agent, gc.nogc()); | ||
| let promise = promise_capability.promise().scope(agent, gc.nogc()); | ||
| // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). | ||
| // NOTE: Promise capability resolve never throws in Nova's internal model. | ||
| promise_capability.unbind().resolve(agent, x, gc.reborrow()); | ||
| // 4. Return promiseCapability.[[Promise]]. | ||
| // SAFETY: Not shared. | ||
| Ok(unsafe { promise.take(agent) }.bind(gc.into_nogc())) | ||
| } | ||
|
|
||
| /// Create a new resolved Promise. | ||
| pub(crate) fn new_resolved(agent: &mut Agent, value: Value<'a>) -> Self { | ||
| agent.heap.create(PromiseHeapData { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: Unnecessary code churn from moving two functions around.