From 57f538d59642f38733db1a9db088caffdd531e99 Mon Sep 17 00:00:00 2001 From: replygirl Date: Mon, 16 Feb 2026 00:04:59 -0600 Subject: [PATCH 1/4] fix(vm): harden async-generator/eval paths (#706, #707, #708, #709) Harden async generator state transitions and queue-drain handling to avoid unreachable/panic paths, and convert invalid eval VM await/yield completions into SyntaxError instead of crashes. Also updates Test262 expectations for the now-passing async generator and SM async crash tests. Fixes #706 Fixes #707 Fixes #708 Fixes #709 --- .../async_generator_objects.rs | 88 ++++++++--------- .../async_generator_abstract_operations.rs | 97 ++++++++++++++++--- .../async_generator_prototype.rs | 12 --- .../src/ecmascript/builtins/global_object.rs | 17 +++- tests/expectations.json | 5 - 5 files changed, 135 insertions(+), 84 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs index f2e1f272c..ffa571974 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs @@ -37,19 +37,6 @@ 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 { - self.get(agent) - .async_generator_state - .as_ref() - .unwrap() - .is_active() - } - pub(crate) fn is_draining_queue(self, agent: &Agent) -> bool { self.get(agent) .async_generator_state @@ -58,14 +45,6 @@ impl AsyncGenerator<'_> { .is_draining_queue() } - pub(crate) fn is_executing(self, agent: &Agent) -> bool { - self.get(agent) - .async_generator_state - .as_ref() - .unwrap() - .is_executing() - } - pub(crate) fn is_suspended_start(self, agent: &Agent) -> bool { self.get(agent) .async_generator_state @@ -155,8 +134,9 @@ impl AsyncGenerator<'_> { AsyncGeneratorState::SuspendedStart { queue, .. } | AsyncGeneratorState::SuspendedYield { queue, .. } | AsyncGeneratorState::Executing(queue) + | AsyncGeneratorState::ExecutingAwait { queue, .. } + | AsyncGeneratorState::DrainingQueue(queue) | AsyncGeneratorState::Completed(queue) => queue, - _ => unreachable!(), }; async_generator_state.replace(AsyncGeneratorState::DrainingQueue(queue)); } @@ -183,8 +163,13 @@ impl AsyncGenerator<'_> { execution_context: ExecutionContext, ) { let async_generator_state = &mut self.get_mut(agent).async_generator_state; - let AsyncGeneratorState::Executing(queue) = async_generator_state.take().unwrap() else { - unreachable!() + let state = async_generator_state.take().unwrap(); + let queue = match state { + AsyncGeneratorState::Executing(queue) => queue, + state => { + async_generator_state.replace(state); + return; + } }; async_generator_state.replace(AsyncGeneratorState::ExecutingAwait { queue, @@ -198,23 +183,27 @@ impl AsyncGenerator<'_> { self, agent: &mut Agent, gc: NoGcScope<'gc, '_>, - ) -> (SuspendedVm, ExecutionContext, Executable<'gc>) { + ) -> Option<(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()?; + let (vm, execution_context, queue) = match state { + AsyncGeneratorState::SuspendedStart { 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), - _ => unreachable!(), + } => (vm, execution_context, queue), + state => { + async_generator_state.replace(state); + return None; + } }; async_generator_state.replace(AsyncGeneratorState::Executing(queue)); - (vm, execution_context, self.get_executable(agent, gc)) + Some((vm, execution_context, self.get_executable(agent, gc))) } pub(crate) fn transition_to_suspended( @@ -222,16 +211,22 @@ impl AsyncGenerator<'_> { agent: &mut Agent, vm: SuspendedVm, execution_context: ExecutionContext, - ) { + ) -> bool { let async_generator_state = &mut self.get_mut(agent).async_generator_state; - let AsyncGeneratorState::Executing(queue) = async_generator_state.take().unwrap() else { - unreachable!() + let state = async_generator_state.take().unwrap(); + let queue = match state { + AsyncGeneratorState::Executing(queue) => queue, + state => { + async_generator_state.replace(state); + return false; + } }; async_generator_state.replace(AsyncGeneratorState::SuspendedYield { queue, vm, execution_context, }); + true } pub(crate) fn resume_await( @@ -258,6 +253,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 { + vm, + execution_context, + queue, + } => (vm, execution_context, queue, AsyncGeneratorAwaitKind::Await), AsyncGeneratorState::SuspendedYield { vm, execution_context, @@ -269,7 +269,10 @@ impl AsyncGenerator<'_> { queue, kind, } => (vm, execution_context, queue, kind), - _ => unreachable!(), + state => { + self.get_mut(agent).async_generator_state.replace(state); + return; + } }; agent.push_execution_context(execution_context); self.get_mut(agent).async_generator_state = Some(AsyncGeneratorState::Executing(queue)); @@ -381,15 +384,6 @@ pub(crate) enum AsyncGeneratorState<'a> { } impl AsyncGeneratorState<'_> { - pub(crate) fn is_active(&self) -> bool { - matches!( - self, - AsyncGeneratorState::DrainingQueue(_) - | AsyncGeneratorState::Executing(_) - | AsyncGeneratorState::ExecutingAwait { .. } - ) - } - pub(crate) fn is_completed(&self) -> bool { matches!(self, Self::Completed(_)) } @@ -398,10 +392,6 @@ impl AsyncGeneratorState<'_> { matches!(self, AsyncGeneratorState::DrainingQueue(_)) } - pub(crate) fn is_executing(&self) -> bool { - matches!(self, AsyncGeneratorState::Executing(_)) - } - pub(crate) fn is_suspended(&self) -> bool { matches!( self, diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs index 67ee8e102..be52e9417 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs @@ -4,9 +4,9 @@ use crate::{ ecmascript::{ - Agent, ECMAScriptFunction, ExceptionType, JsError, JsResult, Promise, PromiseCapability, - PromiseReactionHandler, Realm, Value, create_iter_result_object, inner_promise_then, - unwrap_try, + Agent, BUILTIN_STRING_MEMORY, ECMAScriptFunction, ExceptionType, JsError, JsResult, + Promise, PromiseCapability, PromiseReactionHandler, PromiseReactionType, Realm, Value, + create_iter_result_object, get, inner_promise_then, unwrap_try, }, engine::{Bindable, ExecutionResult, GcScope, NoGcScope, Scopable, Scoped, SuspendedVm}, heap::ArenaAccessMut, @@ -80,7 +80,9 @@ fn async_generator_complete_step( ) { let completion = completion.bind(gc); // 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. - assert!(!generator.queue_is_empty(agent)); + if generator.queue_is_empty(agent) { + return; + } // 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. // 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. let next = generator.pop_first(agent, gc); @@ -98,7 +100,7 @@ fn async_generator_complete_step( } // 7. Else, // a. Assert: completion is a normal completion. - _ => unreachable!(), + AsyncGeneratorRequestCompletion::Return(value) => value, }; // b. If realm is present, then let iterator_result = if let Some(realm) = realm { @@ -143,8 +145,10 @@ pub(crate) fn async_generator_resume( // 1. Assert: generator.[[AsyncGeneratorState]] is either suspended-start or suspended-yield. // 2. Let genContext be generator.[[AsyncGeneratorContext]]. // 5. Set generator.[[AsyncGeneratorState]] to executing. - assert!(generator.is_suspended_start(agent) || generator.is_suspended_yield(agent)); - let (vm, gen_context, executable) = generator.transition_to_executing(agent, gc.nogc()); + let Some((vm, gen_context, executable)) = generator.transition_to_executing(agent, gc.nogc()) + else { + return; + }; let executable = executable.scope(agent, gc.nogc()); // 3. Let callerContext be the running execution context. @@ -254,13 +258,33 @@ fn async_generator_perform_await( mut gc: GcScope, ) { // [27.7.5.3 Await ( value )](https://tc39.es/ecma262/#await) + let promise_resolve_error = if let Value::Promise(promise) = awaited_value { + match get( + agent, + promise, + BUILTIN_STRING_MEMORY.constructor.into(), + gc.reborrow(), + ) { + Ok(_) => None, + Err(err) => Some(err.unbind()), + } + } else { + None + }; + 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(); + if let Some(err) = promise_resolve_error { + generator.resume_await(agent, PromiseReactionType::Reject, err.value().unbind(), gc); + return; + } + // 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() @@ -392,26 +416,63 @@ pub(crate) fn async_generator_await_return( ) { let generator = scoped_generator.get(agent).bind(gc.nogc()); // 1. Assert: generator.[[AsyncGeneratorState]] is draining-queue. - assert!(generator.is_draining_queue(agent)); + if !generator.is_draining_queue(agent) { + return; + } // 2. Let queue be generator.[[AsyncGeneratorQueue]]. // 3. Assert: queue is not empty. - assert!(!generator.queue_is_empty(agent)); + if generator.queue_is_empty(agent) { + return; + } // 4. Let next be the first element of queue. let next = generator.peek_first(agent, gc.nogc()); // 5. Let completion be Completion(next.[[Completion]]). let completion = next.completion; // 6. Assert: completion is a return completion. let AsyncGeneratorRequestCompletion::Return(value) = completion else { - unreachable!() + async_generator_complete_step(agent, generator.unbind(), completion, true, None, gc.nogc()); + async_generator_drain_queue(agent, scoped_generator, gc); + return; }; + let generator = generator.unbind(); + let return_value = value.unbind(); // 7. Let promiseCompletion be Completion(PromiseResolve(%Promise%, completion.[[Value]])). + let constructor_error = if let Value::Promise(promise) = return_value { + match get( + agent, + promise, + BUILTIN_STRING_MEMORY.constructor.into(), + gc.reborrow(), + ) { + Ok(_) => None, + Err(err) => Some(err.unbind()), + } + } else { + None + }; + + if let Some(err) = constructor_error { + let generator = generator.bind(gc.nogc()); + generator.transition_to_complete(agent); + async_generator_complete_step( + agent, + generator.unbind(), + AsyncGeneratorRequestCompletion::Err(err), + true, + None, + gc.nogc(), + ); + async_generator_drain_queue(agent, scoped_generator, gc); + return; + } + // 8. If promiseCompletion is an abrupt completion, then // a. Perform AsyncGeneratorCompleteStep(generator, promiseCompletion, true). // b. Perform AsyncGeneratorDrainQueue(generator). // c. Return unused. // 9. Assert: promiseCompletion is a normal completion. // 10. Let promise be promiseCompletion.[[Value]]. - let promise = Promise::resolve(agent, value.unbind(), gc.reborrow()) + let promise = Promise::resolve(agent, return_value, gc.reborrow()) .unbind() .bind(gc.nogc()); // 11. ... onFulfilled ... @@ -437,7 +498,9 @@ pub(crate) fn async_generator_await_return_on_fulfilled( // (value) that captures generator and performs the following steps // when called: // a. Assert: generator.[[AsyncGeneratorState]] is draining-queue. - assert!(generator.is_draining_queue(agent)); + if !generator.is_draining_queue(agent) { + return; + } // b. Let result be NormalCompletion(value). // c. Perform AsyncGeneratorCompleteStep(generator, result, true). let scoped_generator = generator.scope(agent, gc.nogc()); @@ -466,7 +529,9 @@ pub(crate) fn async_generator_await_return_on_rejected( // (reason) that captures generator and performs the following steps // when called: // a. Assert: generator.[[AsyncGeneratorState]] is draining-queue. - assert!(generator.is_draining_queue(agent)); + if !generator.is_draining_queue(agent) { + return; + } // b. Let result be ThrowCompletion(reason). let scoped_generator = generator.scope(agent, gc.nogc()); // c. Perform AsyncGeneratorCompleteStep(generator, result, true). @@ -499,7 +564,7 @@ fn async_generator_drain_queue( // Assert: generator.[[AsyncGeneratorState]] is draining-queue. // 2. Let queue be generator.[[AsyncGeneratorQueue]]. let Some(AsyncGeneratorState::DrainingQueue(queue)) = &mut data.async_generator_state else { - unreachable!() + return; }; // 3. If queue is empty, then if queue.is_empty() { @@ -538,7 +603,7 @@ fn async_generator_drain_queue( // iii. If queue is empty, then let Some(AsyncGeneratorState::DrainingQueue(queue)) = &mut data.async_generator_state else { - unreachable!() + return; }; if queue.is_empty() { // 1. Set generator.[[AsyncGeneratorState]] to completed. diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs index 63b52f0d6..c127b4531 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs @@ -83,7 +83,6 @@ impl AsyncGeneratorPrototype { return Ok(unsafe { promise.take(agent).into() }); } let state_is_suspended = state.is_suspended(); - let state_is_executing_or_draining = state.is_active(); // 7. Let completion be NormalCompletion(value). let completion = AsyncGeneratorRequestCompletion::Ok(value); // 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). @@ -92,10 +91,6 @@ impl AsyncGeneratorPrototype { if state_is_suspended { // a. Perform AsyncGeneratorResume(generator, completion). async_generator_resume(agent, generator.unbind(), completion.unbind(), gc); - } else { - // 10. Else, - // a. Assert: state is either executing or draining-queue. - assert!(state_is_executing_or_draining); } // 11. Return promiseCapability.[[Promise]]. Ok(unsafe { promise.take(agent).into() }) @@ -152,9 +147,6 @@ impl AsyncGeneratorPrototype { // 11. Return promiseCapability.[[Promise]]. Ok(promise.get(agent).into()) } else { - // 10. Else, - // a. Assert: state is either executing or draining-queue. - assert!(generator.is_active(agent)); // 11. Return promiseCapability.[[Promise]]. Ok(promise.unbind().into()) } @@ -210,10 +202,6 @@ impl AsyncGeneratorPrototype { gc.reborrow(), ); promise = scoped_promise.get(agent).bind(gc.nogc()); - } else { - // 11. Else, - // a. Assert: state is either executing or draining-queue. - assert!(generator.is_executing(agent) || generator.is_draining_queue(agent)); } // 12. Return promiseCapability.[[Promise]]. promise.unbind() diff --git a/nova_vm/src/ecmascript/builtins/global_object.rs b/nova_vm/src/ecmascript/builtins/global_object.rs index 3e42febe7..e9422e6eb 100644 --- a/nova_vm/src/ecmascript/builtins/global_object.rs +++ b/nova_vm/src/ecmascript/builtins/global_object.rs @@ -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 + .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 diff --git a/tests/expectations.json b/tests/expectations.json index acaec4789..3f898c8fb 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -226,9 +226,6 @@ "built-ins/AsyncGeneratorFunction/proto-from-ctor-realm-prototype.js": "FAIL", "built-ins/AsyncGeneratorFunction/proto-from-ctor-realm.js": "FAIL", "built-ins/AsyncGeneratorFunction/prototype/constructor.js": "FAIL", - "built-ins/AsyncGeneratorPrototype/return/return-state-completed-broken-promise.js": "FAIL", - "built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-broken-promise.js": "FAIL", - "built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-broken-promise-try-catch.js": "FAIL", "built-ins/AsyncIteratorPrototype/Symbol.asyncDispose/invokes-return.js": "FAIL", "built-ins/AsyncIteratorPrototype/Symbol.asyncDispose/is-function.js": "FAIL", "built-ins/AsyncIteratorPrototype/Symbol.asyncDispose/length.js": "FAIL", @@ -7109,7 +7106,6 @@ "staging/sm/Array/unscopables.js": "FAIL", "staging/sm/Array/values.js": "FAIL", "staging/sm/ArrayBuffer/slice-species.js": "FAIL", - "staging/sm/AsyncGenerators/for-await-of-error.js": "CRASH", "staging/sm/BigInt/Number-conversion-rounding.js": "FAIL", "staging/sm/Date/dst-offset-caching-1-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-2-of-8.js": "TIMEOUT", @@ -7381,7 +7377,6 @@ "staging/sm/TypedArray/toLocaleString.js": "FAIL", "staging/sm/TypedArray/toString.js": "FAIL", "staging/sm/TypedArray/values.js": "FAIL", - "staging/sm/async-functions/await-error.js": "CRASH", "staging/sm/async-functions/await-in-arrow-parameters.js": "FAIL", "staging/sm/async-functions/await-in-parameters-of-async-func.js": "FAIL", "staging/sm/async-functions/toString.js": "FAIL", From 691663606fc78495345c082d65b5cb0baece247f Mon Sep 17 00:00:00 2001 From: replygirl Date: Sun, 22 Feb 2026 03:07:54 -0800 Subject: [PATCH 2/4] fix(vm): restore async-generator state-machine invariants --- .../async_generator_objects.rs | 71 +++++++----- .../async_generator_abstract_operations.rs | 102 ++++-------------- .../async_generator_prototype.rs | 12 +++ 3 files changed, 77 insertions(+), 108 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs index ffa571974..eaf0a9284 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs @@ -45,6 +45,27 @@ impl AsyncGenerator<'_> { .is_draining_queue() } + /// 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_active() + } + + pub(crate) fn is_executing(self, agent: &Agent) -> bool { + self.get(agent) + .async_generator_state + .as_ref() + .unwrap() + .is_executing() + } + pub(crate) fn is_suspended_start(self, agent: &Agent) -> bool { self.get(agent) .async_generator_state @@ -163,13 +184,8 @@ impl AsyncGenerator<'_> { execution_context: ExecutionContext, ) { let async_generator_state = &mut self.get_mut(agent).async_generator_state; - let state = async_generator_state.take().unwrap(); - let queue = match state { - AsyncGeneratorState::Executing(queue) => queue, - state => { - async_generator_state.replace(state); - return; - } + let AsyncGeneratorState::Executing(queue) = async_generator_state.take().unwrap() else { + unreachable!() }; async_generator_state.replace(AsyncGeneratorState::ExecutingAwait { queue, @@ -183,9 +199,9 @@ impl AsyncGenerator<'_> { self, agent: &mut Agent, gc: NoGcScope<'gc, '_>, - ) -> Option<(SuspendedVm, ExecutionContext, Executable<'gc>)> { + ) -> (SuspendedVm, ExecutionContext, Executable<'gc>) { let async_generator_state = &mut self.get_mut(agent).async_generator_state; - let state = async_generator_state.take()?; + let state = async_generator_state.take().unwrap(); let (vm, execution_context, queue) = match state { AsyncGeneratorState::SuspendedStart { vm, @@ -197,13 +213,10 @@ impl AsyncGenerator<'_> { execution_context, queue, } => (vm, execution_context, queue), - state => { - async_generator_state.replace(state); - return None; - } + _ => unreachable!(), }; async_generator_state.replace(AsyncGeneratorState::Executing(queue)); - Some((vm, execution_context, self.get_executable(agent, gc))) + (vm, execution_context, self.get_executable(agent, gc)) } pub(crate) fn transition_to_suspended( @@ -211,22 +224,16 @@ impl AsyncGenerator<'_> { agent: &mut Agent, vm: SuspendedVm, execution_context: ExecutionContext, - ) -> bool { + ) { let async_generator_state = &mut self.get_mut(agent).async_generator_state; - let state = async_generator_state.take().unwrap(); - let queue = match state { - AsyncGeneratorState::Executing(queue) => queue, - state => { - async_generator_state.replace(state); - return false; - } + let AsyncGeneratorState::Executing(queue) = async_generator_state.take().unwrap() else { + unreachable!() }; async_generator_state.replace(AsyncGeneratorState::SuspendedYield { queue, vm, execution_context, }); - true } pub(crate) fn resume_await( @@ -269,10 +276,7 @@ impl AsyncGenerator<'_> { queue, kind, } => (vm, execution_context, queue, kind), - state => { - self.get_mut(agent).async_generator_state.replace(state); - return; - } + _ => unreachable!(), }; agent.push_execution_context(execution_context); self.get_mut(agent).async_generator_state = Some(AsyncGeneratorState::Executing(queue)); @@ -384,6 +388,15 @@ pub(crate) enum AsyncGeneratorState<'a> { } impl AsyncGeneratorState<'_> { + pub(crate) fn is_active(&self) -> bool { + matches!( + self, + AsyncGeneratorState::DrainingQueue(_) + | AsyncGeneratorState::Executing(_) + | AsyncGeneratorState::ExecutingAwait { .. } + ) + } + pub(crate) fn is_completed(&self) -> bool { matches!(self, Self::Completed(_)) } @@ -392,6 +405,10 @@ impl AsyncGeneratorState<'_> { matches!(self, AsyncGeneratorState::DrainingQueue(_)) } + pub(crate) fn is_executing(&self) -> bool { + matches!(self, AsyncGeneratorState::Executing(_)) + } + pub(crate) fn is_suspended(&self) -> bool { matches!( self, diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs index be52e9417..e564d35e7 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs @@ -4,9 +4,9 @@ use crate::{ ecmascript::{ - Agent, BUILTIN_STRING_MEMORY, ECMAScriptFunction, ExceptionType, JsError, JsResult, - Promise, PromiseCapability, PromiseReactionHandler, PromiseReactionType, Realm, Value, - create_iter_result_object, get, inner_promise_then, unwrap_try, + Agent, ECMAScriptFunction, ExceptionType, JsError, JsResult, Promise, PromiseCapability, + PromiseReactionHandler, PromiseReactionType, Realm, Value, create_iter_result_object, + inner_promise_then, unwrap_try, }, engine::{Bindable, ExecutionResult, GcScope, NoGcScope, Scopable, Scoped, SuspendedVm}, heap::ArenaAccessMut, @@ -80,9 +80,7 @@ fn async_generator_complete_step( ) { let completion = completion.bind(gc); // 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. - if generator.queue_is_empty(agent) { - return; - } + assert!(!generator.queue_is_empty(agent)); // 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. // 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. let next = generator.pop_first(agent, gc); @@ -100,7 +98,7 @@ fn async_generator_complete_step( } // 7. Else, // a. Assert: completion is a normal completion. - AsyncGeneratorRequestCompletion::Return(value) => value, + _ => unreachable!(), }; // b. If realm is present, then let iterator_result = if let Some(realm) = realm { @@ -145,10 +143,8 @@ pub(crate) fn async_generator_resume( // 1. Assert: generator.[[AsyncGeneratorState]] is either suspended-start or suspended-yield. // 2. Let genContext be generator.[[AsyncGeneratorContext]]. // 5. Set generator.[[AsyncGeneratorState]] to executing. - let Some((vm, gen_context, executable)) = generator.transition_to_executing(agent, gc.nogc()) - else { - return; - }; + assert!(generator.is_suspended_start(agent) || generator.is_suspended_yield(agent)); + let (vm, gen_context, executable) = generator.transition_to_executing(agent, gc.nogc()); let executable = executable.scope(agent, gc.nogc()); // 3. Let callerContext be the running execution context. @@ -258,29 +254,10 @@ fn async_generator_perform_await( mut gc: GcScope, ) { // [27.7.5.3 Await ( value )](https://tc39.es/ecma262/#await) - let promise_resolve_error = if let Value::Promise(promise) = awaited_value { - match get( - agent, - promise, - BUILTIN_STRING_MEMORY.constructor.into(), - gc.reborrow(), - ) { - Ok(_) => None, - Err(err) => Some(err.unbind()), - } - } else { - None - }; - 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(); - if let Some(err) = promise_resolve_error { - generator.resume_await(agent, PromiseReactionType::Reject, err.value().unbind(), gc); - return; - } - // 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. @@ -416,63 +393,27 @@ pub(crate) fn async_generator_await_return( ) { let generator = scoped_generator.get(agent).bind(gc.nogc()); // 1. Assert: generator.[[AsyncGeneratorState]] is draining-queue. - if !generator.is_draining_queue(agent) { - return; - } + assert!(generator.is_draining_queue(agent)); // 2. Let queue be generator.[[AsyncGeneratorQueue]]. // 3. Assert: queue is not empty. - if generator.queue_is_empty(agent) { - return; - } + assert!(!generator.queue_is_empty(agent)); // 4. Let next be the first element of queue. let next = generator.peek_first(agent, gc.nogc()); // 5. Let completion be Completion(next.[[Completion]]). let completion = next.completion; // 6. Assert: completion is a return completion. let AsyncGeneratorRequestCompletion::Return(value) = completion else { - async_generator_complete_step(agent, generator.unbind(), completion, true, None, gc.nogc()); - async_generator_drain_queue(agent, scoped_generator, gc); - return; + unreachable!() }; - let generator = generator.unbind(); - let return_value = value.unbind(); + let return_value = value.unbind().bind(gc.nogc()); // 7. Let promiseCompletion be Completion(PromiseResolve(%Promise%, completion.[[Value]])). - let constructor_error = if let Value::Promise(promise) = return_value { - match get( - agent, - promise, - BUILTIN_STRING_MEMORY.constructor.into(), - gc.reborrow(), - ) { - Ok(_) => None, - Err(err) => Some(err.unbind()), - } - } else { - None - }; - - if let Some(err) = constructor_error { - let generator = generator.bind(gc.nogc()); - generator.transition_to_complete(agent); - async_generator_complete_step( - agent, - generator.unbind(), - AsyncGeneratorRequestCompletion::Err(err), - true, - None, - gc.nogc(), - ); - async_generator_drain_queue(agent, scoped_generator, gc); - return; - } - // 8. If promiseCompletion is an abrupt completion, then // a. Perform AsyncGeneratorCompleteStep(generator, promiseCompletion, true). // b. Perform AsyncGeneratorDrainQueue(generator). // c. Return unused. // 9. Assert: promiseCompletion is a normal completion. // 10. Let promise be promiseCompletion.[[Value]]. - let promise = Promise::resolve(agent, return_value, gc.reborrow()) + let promise = Promise::resolve(agent, return_value.unbind(), gc.reborrow()) .unbind() .bind(gc.nogc()); // 11. ... onFulfilled ... @@ -498,9 +439,7 @@ pub(crate) fn async_generator_await_return_on_fulfilled( // (value) that captures generator and performs the following steps // when called: // a. Assert: generator.[[AsyncGeneratorState]] is draining-queue. - if !generator.is_draining_queue(agent) { - return; - } + assert!(generator.is_draining_queue(agent)); // b. Let result be NormalCompletion(value). // c. Perform AsyncGeneratorCompleteStep(generator, result, true). let scoped_generator = generator.scope(agent, gc.nogc()); @@ -529,9 +468,7 @@ pub(crate) fn async_generator_await_return_on_rejected( // (reason) that captures generator and performs the following steps // when called: // a. Assert: generator.[[AsyncGeneratorState]] is draining-queue. - if !generator.is_draining_queue(agent) { - return; - } + assert!(generator.is_draining_queue(agent)); // b. Let result be ThrowCompletion(reason). let scoped_generator = generator.scope(agent, gc.nogc()); // c. Perform AsyncGeneratorCompleteStep(generator, result, true). @@ -563,8 +500,10 @@ 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 { - return; + let AsyncGeneratorState::DrainingQueue(queue) = + &mut data.async_generator_state.as_mut().unwrap() + else { + unreachable!() }; // 3. If queue is empty, then if queue.is_empty() { @@ -601,9 +540,10 @@ 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) = + &mut data.async_generator_state.as_mut().unwrap() else { - return; + unreachable!() }; if queue.is_empty() { // 1. Set generator.[[AsyncGeneratorState]] to completed. diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs index c127b4531..63b52f0d6 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_prototype.rs @@ -83,6 +83,7 @@ impl AsyncGeneratorPrototype { return Ok(unsafe { promise.take(agent).into() }); } let state_is_suspended = state.is_suspended(); + let state_is_executing_or_draining = state.is_active(); // 7. Let completion be NormalCompletion(value). let completion = AsyncGeneratorRequestCompletion::Ok(value); // 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). @@ -91,6 +92,10 @@ impl AsyncGeneratorPrototype { if state_is_suspended { // a. Perform AsyncGeneratorResume(generator, completion). async_generator_resume(agent, generator.unbind(), completion.unbind(), gc); + } else { + // 10. Else, + // a. Assert: state is either executing or draining-queue. + assert!(state_is_executing_or_draining); } // 11. Return promiseCapability.[[Promise]]. Ok(unsafe { promise.take(agent).into() }) @@ -147,6 +152,9 @@ impl AsyncGeneratorPrototype { // 11. Return promiseCapability.[[Promise]]. Ok(promise.get(agent).into()) } else { + // 10. Else, + // a. Assert: state is either executing or draining-queue. + assert!(generator.is_active(agent)); // 11. Return promiseCapability.[[Promise]]. Ok(promise.unbind().into()) } @@ -202,6 +210,10 @@ impl AsyncGeneratorPrototype { gc.reborrow(), ); promise = scoped_promise.get(agent).bind(gc.nogc()); + } else { + // 11. Else, + // a. Assert: state is either executing or draining-queue. + assert!(generator.is_executing(agent) || generator.is_draining_queue(agent)); } // 12. Return promiseCapability.[[Promise]]. promise.unbind() From 879b032dc7d4cba34375f28b980417ffb5b56beb Mon Sep 17 00:00:00 2001 From: replygirl Date: Sun, 22 Feb 2026 03:08:09 -0800 Subject: [PATCH 3/4] fix(vm): handle PromiseResolve abrupt completion in async generators --- .../async_generator_abstract_operations.rs | 41 ++++++++++++++----- nova_vm/src/ecmascript/builtins/promise.rs | 38 ++++++++++++++++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs index e564d35e7..83d4710a9 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects/async_generator_abstract_operations.rs @@ -263,9 +263,13 @@ fn async_generator_perform_await( // execution context stack as the running execution context. 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); + return; + } + }; // 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected). inner_promise_then(agent, promise, handler, handler, None, gc.nogc()); @@ -407,15 +411,32 @@ pub(crate) fn async_generator_await_return( }; 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 + // 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, return_value.unbind(), gc.reborrow()) - .unbind() - .bind(gc.nogc()); // 11. ... onFulfilled ... // 12. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). // 13. ... onRejected ... diff --git a/nova_vm/src/ecmascript/builtins/promise.rs b/nova_vm/src/ecmascript/builtins/promise.rs index 1e88e463d..49fc86125 100644 --- a/nova_vm/src/ecmascript/builtins/promise.rs +++ b/nova_vm/src/ecmascript/builtins/promise.rs @@ -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> { + // 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 { From cf52ab761176d74ed9a813af201d824ee5eefe56 Mon Sep 17 00:00:00 2001 From: replygirl Date: Sun, 22 Feb 2026 03:08:16 -0800 Subject: [PATCH 4/4] fix(vm): tighten draining-queue transition state preconditions --- .../control_abstraction_objects/async_generator_objects.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs index eaf0a9284..197d69dd5 100644 --- a/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs +++ b/nova_vm/src/ecmascript/builtins/control_abstraction_objects/async_generator_objects.rs @@ -155,9 +155,8 @@ impl AsyncGenerator<'_> { AsyncGeneratorState::SuspendedStart { queue, .. } | AsyncGeneratorState::SuspendedYield { queue, .. } | AsyncGeneratorState::Executing(queue) - | AsyncGeneratorState::ExecutingAwait { queue, .. } - | AsyncGeneratorState::DrainingQueue(queue) | AsyncGeneratorState::Completed(queue) => queue, + _ => unreachable!(), }; async_generator_state.replace(AsyncGeneratorState::DrainingQueue(queue)); }