From b14ff35797a1a130847ac026c1bfe2770dd41b0a Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sat, 14 Mar 2026 11:41:00 -0600 Subject: [PATCH 1/2] Make `JsAsyncGenerator` fully type safe --- .../src/builtins/async_generator/mod.rs | 80 ++++++++++++++----- .../src/object/builtins/jsasyncgenerator.rs | 71 ++++++++-------- 2 files changed, 95 insertions(+), 56 deletions(-) diff --git a/core/engine/src/builtins/async_generator/mod.rs b/core/engine/src/builtins/async_generator/mod.rs index 7569b712f48..a0f29087fe5 100644 --- a/core/engine/src/builtins/async_generator/mod.rs +++ b/core/engine/src/builtins/async_generator/mod.rs @@ -26,7 +26,7 @@ use crate::{ vm::{CompletionRecord, GeneratorResumeKind}, }; use boa_gc::{Finalize, Trace}; -use std::collections::VecDeque; +use std::{collections::VecDeque, slice}; use super::{BuiltInBuilder, IntrinsicObject}; @@ -143,6 +143,21 @@ impl AsyncGenerator { }); let generator = if_abrupt_reject_promise!(result, promise_capability, context); + Self::inner_next( + &generator, + promise_capability, + args.get_or_undefined(0).clone(), + context, + ) + .map(JsValue::from) + } + + pub(crate) fn inner_next( + generator: &JsObject, + cap: PromiseCapability, + value: JsValue, + context: &mut Context, + ) -> JsResult { // 5. Let state be generator.[[AsyncGeneratorState]]. let state = generator.borrow().data().state; @@ -152,21 +167,18 @@ impl AsyncGenerator { let iterator_result = create_iter_result_object(JsValue::undefined(), true, context); // b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). - promise_capability.resolve().call( - &JsValue::undefined(), - &[iterator_result], - context, - )?; + cap.resolve() + .call(&JsValue::undefined(), &[iterator_result], context)?; // c. Return promiseCapability.[[Promise]]. - return Ok(promise_capability.promise().clone().into()); + return Ok(cap.promise); } // 7. Let completion be NormalCompletion(value). - let completion = CompletionRecord::Normal(args.get_or_undefined(0).clone()); + let completion = CompletionRecord::Normal(value); // 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). - Self::enqueue(&generator, completion.clone(), promise_capability.clone()); + Self::enqueue(&generator, completion.clone(), cap.clone()); // 9. If state is either suspendedStart or suspendedYield, then if state == AsyncGeneratorState::SuspendedStart @@ -177,7 +189,7 @@ impl AsyncGenerator { } // 11. Return promiseCapability.[[Promise]]. - Ok(promise_capability.promise().clone().into()) + Ok(cap.promise) } /// `AsyncGenerator.prototype.return ( value )` @@ -216,12 +228,26 @@ impl AsyncGenerator { }); let generator = if_abrupt_reject_promise!(result, promise_capability, context); + Self::inner_return( + &generator, + promise_capability, + args.get_or_undefined(0).clone(), + context, + ) + .map(JsValue::from) + } + + pub(crate) fn inner_return( + generator: &JsObject, + cap: PromiseCapability, + return_value: JsValue, + context: &mut Context, + ) -> JsResult { // 5. Let completion be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. - let return_value = args.get_or_undefined(0).clone(); let completion = CompletionRecord::Return(return_value.clone()); // 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). - Self::enqueue(&generator, completion.clone(), promise_capability.clone()); + Self::enqueue(&generator, completion.clone(), cap.clone()); // 7. Let state be generator.[[AsyncGeneratorState]]. let state = generator.borrow().data().state; @@ -243,7 +269,7 @@ impl AsyncGenerator { // a. Assert: state is either executing or draining-queue. // 11. Return promiseCapability.[[Promise]]. - Ok(promise_capability.promise().clone().into()) + Ok(cap.promise) } /// `AsyncGenerator.prototype.throw ( exception )` @@ -281,6 +307,21 @@ impl AsyncGenerator { .into() }); let generator = if_abrupt_reject_promise!(result, promise_capability, context); + Self::inner_throw( + &generator, + promise_capability, + args.get_or_undefined(0).clone(), + context, + ) + .map(JsValue::from) + } + + pub(crate) fn inner_throw( + generator: &JsObject, + cap: PromiseCapability, + error_value: JsValue, + context: &mut Context, + ) -> JsResult { let mut r#gen = generator.borrow_mut(); // 5. Let state be generator.[[AsyncGeneratorState]]. @@ -301,22 +342,21 @@ impl AsyncGenerator { // 7. If state is completed, then if state == AsyncGeneratorState::Completed { // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « exception »). - promise_capability.reject().call( + cap.reject().call( &JsValue::undefined(), - &[args.get_or_undefined(0).clone()], + slice::from_ref(&error_value), context, )?; // b. Return promiseCapability.[[Promise]]. - return Ok(promise_capability.promise().clone().into()); + return Ok(cap.promise().clone().into()); } // 8. Let completion be ThrowCompletion(exception). - let completion = - CompletionRecord::Throw(JsError::from_opaque(args.get_or_undefined(0).clone())); + let completion = CompletionRecord::Throw(JsError::from_opaque(error_value)); // 9. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). - Self::enqueue(&generator, completion.clone(), promise_capability.clone()); + Self::enqueue(&generator, completion.clone(), cap.clone()); // 10. If state is suspended-yield, then if state == AsyncGeneratorState::SuspendedYield { @@ -328,7 +368,7 @@ impl AsyncGenerator { // a. Assert: state is either executing or draining-queue. // 12. Return promiseCapability.[[Promise]]. - Ok(promise_capability.promise().clone().into()) + Ok(cap.promise().clone().into()) } /// `AsyncGeneratorEnqueue ( generator, completion, promiseCapability )` diff --git a/core/engine/src/object/builtins/jsasyncgenerator.rs b/core/engine/src/object/builtins/jsasyncgenerator.rs index 63b2697a1a4..d87768c5cac 100644 --- a/core/engine/src/object/builtins/jsasyncgenerator.rs +++ b/core/engine/src/object/builtins/jsasyncgenerator.rs @@ -1,16 +1,20 @@ //! A Rust API wrapper for Boa's `AsyncGenerator` Builtin ECMAScript Object use super::JsPromise; use crate::{ - Context, JsNativeError, JsResult, JsValue, builtins::async_generator::AsyncGenerator, - object::JsObject, value::TryFromJs, + Context, JsNativeError, JsResult, JsValue, + builtins::{async_generator::AsyncGenerator, promise::PromiseCapability}, + js_error, + object::JsObject, + value::TryFromJs, }; use boa_gc::{Finalize, Trace}; use std::ops::Deref; /// `JsAsyncGenerator` provides a wrapper for Boa's implementation of the ECMAScript `AsyncGenerator` builtin object. #[derive(Debug, Clone, Trace, Finalize)] +#[boa_gc(unsafe_no_drop)] pub struct JsAsyncGenerator { - inner: JsObject, + inner: JsObject, } impl JsAsyncGenerator { @@ -22,13 +26,10 @@ impl JsAsyncGenerator { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator #[inline] pub fn from_object(object: JsObject) -> JsResult { - if object.is::() { - Ok(Self { inner: object }) - } else { - Err(JsNativeError::typ() - .with_message("object is not an AsyncGenerator") - .into()) - } + object + .downcast::() + .map(|inner| Self { inner }) + .map_err(|_| js_error!(TypeError: "object is not an AsyncGenerator object")) } /// Calls `AsyncGenerator.prototype.next()`. @@ -41,14 +42,14 @@ impl JsAsyncGenerator { where T: Into, { - let value = AsyncGenerator::next(&self.inner.clone().into(), &[value.into()], context)?; - let obj = value - .as_object() - .ok_or_else(|| { - JsNativeError::typ().with_message("async generator did not return a Promise") - })? - .clone(); - JsPromise::from_object(obj) + let (typed_promise, functions) = JsPromise::new_pending(context); + let capability = PromiseCapability { + functions, + promise: (&*typed_promise).clone().upcast(), + }; + AsyncGenerator::inner_next(&self.inner, capability, value.into(), context)?; + + Ok(typed_promise) } /// Calls `AsyncGenerator.prototype.return()`. @@ -61,14 +62,13 @@ impl JsAsyncGenerator { where T: Into, { - let value = AsyncGenerator::r#return(&self.inner.clone().into(), &[value.into()], context)?; - let obj = value - .as_object() - .ok_or_else(|| { - JsNativeError::typ().with_message("async generator did not return a Promise") - })? - .clone(); - JsPromise::from_object(obj) + let (typed_promise, functions) = JsPromise::new_pending(context); + let capability = PromiseCapability { + functions, + promise: (&*typed_promise).clone().upcast(), + }; + AsyncGenerator::inner_return(&self.inner, capability, value.into(), context)?; + Ok(typed_promise) } /// Calls `AsyncGenerator.prototype.throw()`. @@ -81,21 +81,20 @@ impl JsAsyncGenerator { where T: Into, { - let value = AsyncGenerator::throw(&self.inner.clone().into(), &[value.into()], context)?; - let obj = value - .as_object() - .ok_or_else(|| { - JsNativeError::typ().with_message("async generator did not return a Promise") - })? - .clone(); - JsPromise::from_object(obj) + let (typed_promise, functions) = JsPromise::new_pending(context); + let capability = PromiseCapability { + functions, + promise: (&*typed_promise).clone().upcast(), + }; + AsyncGenerator::inner_throw(&self.inner, capability, value.into(), context)?; + Ok(typed_promise) } } impl From for JsObject { #[inline] fn from(o: JsAsyncGenerator) -> Self { - o.inner.clone() + o.inner.upcast() } } @@ -107,7 +106,7 @@ impl From for JsValue { } impl Deref for JsAsyncGenerator { - type Target = JsObject; + type Target = JsObject; #[inline] fn deref(&self) -> &Self::Target { From 2a2c77efe96783ae3aeec6a254449ad3a6b815df Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sat, 14 Mar 2026 12:55:34 -0600 Subject: [PATCH 2/2] cargo clippy --- .../engine/src/builtins/async_generator/mod.rs | 18 +++++++++--------- .../src/object/builtins/jsasyncgenerator.rs | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/engine/src/builtins/async_generator/mod.rs b/core/engine/src/builtins/async_generator/mod.rs index a0f29087fe5..c3d99c3187b 100644 --- a/core/engine/src/builtins/async_generator/mod.rs +++ b/core/engine/src/builtins/async_generator/mod.rs @@ -178,14 +178,14 @@ impl AsyncGenerator { let completion = CompletionRecord::Normal(value); // 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). - Self::enqueue(&generator, completion.clone(), cap.clone()); + Self::enqueue(generator, completion.clone(), cap.clone()); // 9. If state is either suspendedStart or suspendedYield, then if state == AsyncGeneratorState::SuspendedStart || state == AsyncGeneratorState::SuspendedYield { // a. Perform AsyncGeneratorResume(generator, completion). - Self::resume(&generator, completion, context)?; + Self::resume(generator, completion, context)?; } // 11. Return promiseCapability.[[Promise]]. @@ -247,7 +247,7 @@ impl AsyncGenerator { let completion = CompletionRecord::Return(return_value.clone()); // 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). - Self::enqueue(&generator, completion.clone(), cap.clone()); + Self::enqueue(generator, completion.clone(), cap.clone()); // 7. Let state be generator.[[AsyncGeneratorState]]. let state = generator.borrow().data().state; @@ -258,12 +258,12 @@ impl AsyncGenerator { generator.borrow_mut().data_mut().state = AsyncGeneratorState::DrainingQueue; // b. Perform ! AsyncGeneratorAwaitReturn(generator). - Self::await_return(&generator, return_value, context)?; + Self::await_return(generator, return_value, context)?; } // 9. Else if state is suspended-yield, then else if state == AsyncGeneratorState::SuspendedYield { // a. Perform AsyncGeneratorResume(generator, completion). - Self::resume(&generator, completion, context)?; + Self::resume(generator, completion, context)?; } // 10. Else, // a. Assert: state is either executing or draining-queue. @@ -349,26 +349,26 @@ impl AsyncGenerator { )?; // b. Return promiseCapability.[[Promise]]. - return Ok(cap.promise().clone().into()); + return Ok(cap.promise); } // 8. Let completion be ThrowCompletion(exception). let completion = CompletionRecord::Throw(JsError::from_opaque(error_value)); // 9. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). - Self::enqueue(&generator, completion.clone(), cap.clone()); + Self::enqueue(generator, completion.clone(), cap.clone()); // 10. If state is suspended-yield, then if state == AsyncGeneratorState::SuspendedYield { // a. Perform AsyncGeneratorResume(generator, completion). - Self::resume(&generator, completion, context)?; + Self::resume(generator, completion, context)?; } // 11. Else, // a. Assert: state is either executing or draining-queue. // 12. Return promiseCapability.[[Promise]]. - Ok(cap.promise().clone().into()) + Ok(cap.promise) } /// `AsyncGeneratorEnqueue ( generator, completion, promiseCapability )` diff --git a/core/engine/src/object/builtins/jsasyncgenerator.rs b/core/engine/src/object/builtins/jsasyncgenerator.rs index d87768c5cac..8cb567880e0 100644 --- a/core/engine/src/object/builtins/jsasyncgenerator.rs +++ b/core/engine/src/object/builtins/jsasyncgenerator.rs @@ -45,7 +45,7 @@ impl JsAsyncGenerator { let (typed_promise, functions) = JsPromise::new_pending(context); let capability = PromiseCapability { functions, - promise: (&*typed_promise).clone().upcast(), + promise: JsObject::clone(&typed_promise).clone().upcast(), }; AsyncGenerator::inner_next(&self.inner, capability, value.into(), context)?; @@ -65,7 +65,7 @@ impl JsAsyncGenerator { let (typed_promise, functions) = JsPromise::new_pending(context); let capability = PromiseCapability { functions, - promise: (&*typed_promise).clone().upcast(), + promise: JsObject::clone(&typed_promise).upcast(), }; AsyncGenerator::inner_return(&self.inner, capability, value.into(), context)?; Ok(typed_promise) @@ -84,7 +84,7 @@ impl JsAsyncGenerator { let (typed_promise, functions) = JsPromise::new_pending(context); let capability = PromiseCapability { functions, - promise: (&*typed_promise).clone().upcast(), + promise: JsObject::clone(&typed_promise).clone().upcast(), }; AsyncGenerator::inner_throw(&self.inner, capability, value.into(), context)?; Ok(typed_promise)