diff --git a/core/engine/src/builtins/iterable/async_from_sync_iterator.rs b/core/engine/src/builtins/iterable/async_from_sync_iterator.rs index ead34375b82..7ccf4bb5d09 100644 --- a/core/engine/src/builtins/iterable/async_from_sync_iterator.rs +++ b/core/engine/src/builtins/iterable/async_from_sync_iterator.rs @@ -10,7 +10,7 @@ use crate::{ object::{FunctionObjectBuilder, JsObject}, realm::Realm, string::utf16, - Context, JsArgs, JsData, JsResult, JsValue, + Context, JsArgs, JsData, JsError, JsNativeError, JsResult, JsValue, }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; @@ -129,8 +129,14 @@ impl AsyncFromSyncIterator { // 7. IfAbruptRejectPromise(result, promiseCapability). let result = if_abrupt_reject_promise!(result, promise_capability, context); - // 8. Return AsyncFromSyncIteratorContinuation(result, promiseCapability). - Self::continuation(&result, &promise_capability, context) + // 8. Return AsyncFromSyncIteratorContinuation(result, promiseCapability, syncIteratorRecord, true). + Self::continuation( + &result, + &promise_capability, + sync_iterator_record, + true, + context, + ) } /// `%AsyncFromSyncIteratorPrototype%.return ( [ value ] )` @@ -142,14 +148,15 @@ impl AsyncFromSyncIterator { fn r#return(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let O be the this value. // 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot. - // 4. Let syncIterator be O.[[SyncIteratorRecord]].[[Iterator]]. - let sync_iterator = this + // 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]]. + let sync_iterator_record = this .as_object() .and_then(JsObject::downcast_ref::) .expect("async from sync iterator prototype must be object") .sync_iterator_record - .iterator() .clone(); + // 5. Let syncIterator be syncIteratorRecord.[[Iterator]]. + let sync_iterator = sync_iterator_record.iterator().clone(); // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( @@ -158,14 +165,14 @@ impl AsyncFromSyncIterator { ) .expect("cannot fail with promise constructor"); - // 5. Let return be Completion(GetMethod(syncIterator, "return")). + // 6. Let return be Completion(GetMethod(syncIterator, "return")). let r#return = sync_iterator.get_method(utf16!("return"), context); - // 6. IfAbruptRejectPromise(return, promiseCapability). + // 7. IfAbruptRejectPromise(return, promiseCapability). let r#return = if_abrupt_reject_promise!(r#return, promise_capability, context); let result = match (r#return, args.first()) { - // 7. If return is undefined, then + // 8. If return is undefined, then (None, _) => { // a. Let iterResult be CreateIterResultObject(value, true). let iter_result = @@ -180,27 +187,33 @@ impl AsyncFromSyncIterator { // c. Return promiseCapability.[[Promise]]. return Ok(promise_capability.promise().clone().into()); } - // 8. If value is present, then + // 9. If value is present, then (Some(r#return), Some(value)) => { // a. Let result be Completion(Call(return, syncIterator, « value »)). r#return.call(&sync_iterator.clone().into(), &[value.clone()], context) } - // 9. Else, + // 10. Else, (Some(r#return), None) => { // a. Let result be Completion(Call(return, syncIterator)). r#return.call(&sync_iterator.clone().into(), &[], context) } }; - // 11. If Type(result) is not Object, then - // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + // 12. If Type(result) is not Object, then + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). let result = result.and_then(IteratorResult::from_value); - // 10. IfAbruptRejectPromise(result, promiseCapability). + // 11. IfAbruptRejectPromise(result, promiseCapability). let result = if_abrupt_reject_promise!(result, promise_capability, context); - // 12. Return AsyncFromSyncIteratorContinuation(result, promiseCapability). - Self::continuation(&result, &promise_capability, context) + // 13. Return AsyncFromSyncIteratorContinuation(result, promiseCapability, syncIteratorRecord, false). + Self::continuation( + &result, + &promise_capability, + sync_iterator_record, + false, + context, + ) } /// `%AsyncFromSyncIteratorPrototype%.throw ( [ value ] )` @@ -212,14 +225,15 @@ impl AsyncFromSyncIterator { fn throw(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let O be the this value. // 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot. - // 4. Let syncIterator be O.[[SyncIteratorRecord]].[[Iterator]]. - let sync_iterator = this + // 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]]. + let sync_iterator_record = this .as_object() .and_then(JsObject::downcast_ref::) .expect("async from sync iterator prototype must be object") .sync_iterator_record - .iterator() .clone(); + // 5. Let syncIterator be syncIteratorRecord.[[Iterator]]. + let sync_iterator = sync_iterator_record.iterator().clone(); // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( @@ -228,49 +242,67 @@ impl AsyncFromSyncIterator { ) .expect("cannot fail with promise constructor"); - // 5. Let throw be Completion(GetMethod(syncIterator, "throw")). + // 6. Let throw be Completion(GetMethod(syncIterator, "throw")). let throw = sync_iterator.get_method(utf16!("throw"), context); - // 6. IfAbruptRejectPromise(throw, promiseCapability). + // 7. IfAbruptRejectPromise(throw, promiseCapability). let throw = if_abrupt_reject_promise!(throw, promise_capability, context); let result = match (throw, args.first()) { - // 7. If throw is undefined, then + // 8. If throw is undefined, then (None, _) => { - // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « value »). + // a. NOTE: If syncIterator does not have a throw method, close it to give it a chance to clean up before we reject the capability. + // b. Let closeCompletion be NormalCompletion(empty). + // c. Let result be Completion(IteratorClose(syncIteratorRecord, closeCompletion)). + let result = sync_iterator_record.close(Ok(JsValue::undefined()), context); + // d. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + // e. NOTE: The next step throws a TypeError to indicate that there was a protocol violation: syncIterator does not have a throw method. + // f. NOTE: If closing syncIterator does not throw then the result of that operation is ignored, even if it yields a rejected promise. + // g. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). promise_capability .reject() .call( - &JsValue::Undefined, - &[args.get_or_undefined(0).clone()], + &JsValue::undefined(), + &[JsNativeError::typ() + .with_message("sync iterator does not have a throw method") + .to_opaque(context) + .into()], context, ) .expect("cannot fail according to spec"); - // b. Return promiseCapability.[[Promise]]. + // h. Return promiseCapability.[[Promise]]. return Ok(promise_capability.promise().clone().into()); } - // 8. If value is present, then + // 9. If value is present, then (Some(throw), Some(value)) => { // a. Let result be Completion(Call(throw, syncIterator, « value »)). throw.call(&sync_iterator.clone().into(), &[value.clone()], context) } - // 9. Else, + // 10. Else, (Some(throw), None) => { // a. Let result be Completion(Call(throw, syncIterator)). throw.call(&sync_iterator.clone().into(), &[], context) } }; - // 11. If Type(result) is not Object, then + // 12. If Type(result) is not Object, then // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). let result = result.and_then(IteratorResult::from_value); - // 10. IfAbruptRejectPromise(result, promiseCapability). + // 11. IfAbruptRejectPromise(result, promiseCapability). let result = if_abrupt_reject_promise!(result, promise_capability, context); - // 12. Return AsyncFromSyncIteratorContinuation(result, promiseCapability). - Self::continuation(&result, &promise_capability, context) + // 13. Return Return AsyncFromSyncIteratorContinuation(result, promiseCapability, syncIteratorRecord, true). + Self::continuation( + &result, + &promise_capability, + sync_iterator_record, + true, + context, + ) } /// `AsyncFromSyncIteratorContinuation ( result, promiseCapability )` @@ -282,6 +314,8 @@ impl AsyncFromSyncIterator { fn continuation( result: &IteratorResult, promise_capability: &PromiseCapability, + sync_iterator_record: IteratorRecord, + close_on_rejection: bool, context: &mut Context, ) -> JsResult { // 1. NOTE: Because promiseCapability is derived from the intrinsic %Promise%, @@ -301,18 +335,28 @@ impl AsyncFromSyncIterator { let value = if_abrupt_reject_promise!(value, promise_capability, context); // 6. Let valueWrapper be Completion(PromiseResolve(%Promise%, value)). - let value_wrapper = Promise::promise_resolve( + let value_wrapper = match Promise::promise_resolve( &context.intrinsics().constructors().promise().constructor(), value, context, - ); + ) { + // 7. If valueWrapper is an abrupt completion, done is false, and closeOnRejection is + // true, then + Err(e) if !done && close_on_rejection => { + // a. Set valueWrapper to Completion(IteratorClose(syncIteratorRecord, valueWrapper)). + Err(sync_iterator_record.close(Err(e), context).expect_err( + "closing an iterator with an error must always return an error back", + )) + } + other => other, + }; - // 7. IfAbruptRejectPromise(valueWrapper, promiseCapability). + // 8. IfAbruptRejectPromise(valueWrapper, promiseCapability). let value_wrapper = if_abrupt_reject_promise!(value_wrapper, promise_capability, context); - // 8. Let unwrap be a new Abstract Closure with parameters (value) + // 9. Let unwrap be a new Abstract Closure with parameters (value) // that captures done and performs the following steps when called: - // 9. Let onFulfilled be CreateBuiltinFunction(unwrap, 1, "", « »). + // 10. Let onFulfilled be CreateBuiltinFunction(unwrap, 1, "", « »). let on_fulfilled = FunctionObjectBuilder::new( context.realm(), NativeFunction::from_copy_closure(move |_this, args, context| { @@ -328,20 +372,51 @@ impl AsyncFromSyncIterator { .length(1) .build(); - // 10. NOTE: onFulfilled is used when processing the "value" property of an + // 11. NOTE: onFulfilled is used when processing the "value" property of an // IteratorResult object in order to wait for its value if it is a promise and // re-package the result in a new "unwrapped" IteratorResult object. - // 11. Perform PerformPromiseThen(valueWrapper, onFulfilled, undefined, promiseCapability). + // 12. If done is true, or if closeOnRejection is false, then + let on_rejected = if done || !close_on_rejection { + // a. Let onRejected be undefined. + None + } else { + // 13. Else, + // a. Let closeIterator be a new Abstract Closure with parameters (error) that + // captures syncIteratorRecord and performs the following steps when called: + // b. Let onRejected be CreateBuiltinFunction(closeIterator, 1, "", « »). + // c. NOTE: onRejected is used to close the Iterator when the "value" property of an + // IteratorResult object it yields is a rejected promise. + Some( + FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_copy_closure_with_captures( + |_this, args, iter, context| { + // i. Return ? IteratorClose(syncIteratorRecord, ThrowCompletion(error)). + iter.close( + Err(JsError::from_opaque(args.get_or_undefined(0).clone())), + context, + ) + }, + sync_iterator_record, + ), + ) + .name("") + .length(1) + .build(), + ) + }; + + // 14. Perform PerformPromiseThen(valueWrapper, onFulfilled, undefined, promiseCapability). Promise::perform_promise_then( &value_wrapper, Some(on_fulfilled), - None, + on_rejected, Some(promise_capability.clone()), context, ); - // 12. Return promiseCapability.[[Promise]]. + // 15. Return promiseCapability.[[Promise]]. Ok(promise_capability.promise().clone().into()) } }