Skip to content
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

Close sync iterator when async wrapper yields rejection #3633

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 116 additions & 41 deletions core/engine/src/builtins/iterable/async_from_sync_iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ] )`
Expand All @@ -142,14 +148,15 @@ impl AsyncFromSyncIterator {
fn r#return(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 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::<Self>)
.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(
Expand All @@ -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 =
Expand All @@ -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 ] )`
Expand All @@ -212,14 +225,15 @@ impl AsyncFromSyncIterator {
fn throw(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 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::<Self>)
.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(
Expand All @@ -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 )`
Expand All @@ -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<JsValue> {
// 1. NOTE: Because promiseCapability is derived from the intrinsic %Promise%,
Expand All @@ -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| {
Expand All @@ -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())
}
}
Loading