tc39 / ecma262

Status, process, and documents for ECMA-262
https://tc39.es/ecma262/
Other
15.03k stars 1.28k forks source link

Sync-to-async iterable conversion has more guarantees than plain async iterable conversion #3417

Closed domfarolino closed 4 weeks ago

domfarolino commented 1 month ago

GetIterator(obj, ASYNC) returns an Iterator Record with a [[NextMethod]] that is pulled from the user-supplied object. The next method hopefully returns a Promise, but this requirement is not enforced. Because it is not enforced, specs that convert values to async iterators this way must take care to wrap the resulting value in a Promise. This is standard practice; the Streams Standard does this, and so do the semantics of for await loops: specifically, the head evaluation semantics grab an async iterator record, and then step 6 of the body evaluation gets the next value and unconditionally Await()s it. Await() first wraps the value in a Promise and awaits it, which is necessary in case the value returned from the author's async iterable's next() method is not a Promise.

In other words, the consumer of an async iterable is responsible for wrapping all next() values in a Promise, in case the code author did not do this. This is understandable, but a little annoying I guess, since responsibility is pushed to the consumer when ECMAScript itself could take care of this.

In fact, ECMAScript does take care of this (i.e., ensuring that all next() values are Promises) but only when GetIterator(obj, ASYNC) falls back to the @@iterator implementation (i.e., when @@asyncIterator is not present). In this case and this case only, the fallback prose doesn't just directly use the sync iterator Record for its next() values; instead, it delegates to CreateAsyncFromSyncIterator(), which creates an internal %AsyncFromSyncIteratorPrototype% object whose next() method is guaranteed to return a Promise that resolves to the underlying [[SyncIteratorRecord]]'s actual next value (which should be an Iterator Result).

This automatic Promise-wrapping is quite nice, but it's strange that we only do it for the least kind of async iterable, i.e., sync iterables. Since we have this automatic Promise-wrapping semantics sometimes, it'd be nice if we could extend it to all converted async iterables, so that consumers of GetIterator(obj, ASYNC) could always guarantee that the result of IteratorNext() is a Promise.

If we don't, then since consumers of async iterables are anyways trained to do the wrapping themselves, maybe we can just get rid of the CreateAsyncFromSyncIterator() usage in the sync fallback case. After all, there's no need for [[NextMethod]] to always return a Promise, since consumers can't rely on this in general.

Thoughts?

bakkot commented 1 month ago

The purpose of CreateAsyncFromSyncIterator isn't to guarantee that [[NextMethod]] returns a Promise; rather, it's to lift an IteratorResult<Promise<T>> to Promise<IteratorResult<T>>.

That is, if a sync iterator returns { next: Promise.resolve(0), done: false }, then CreateAsyncFromSyncIterator turns that into Promise.resolve({ next: 0, done: false }). Note that the next field no longer contains a Promise. That, and only that, is the purpose of CreateAsyncFromSyncIterator. The fact that this also happens to guarantee the result is a Promise is just a side-effect.

Consumers generally shouldn't have to care whether the result of next is a Promise; rather, they should just do whatever the local equivalent of await is. await can consume either a Promise or non-Promise value (or other kinds of thenable, for that matter) - as you note, mechanically this is specified in terms of constructing an actual Promise value and then waiting on that (unless the original thing was a real Promise according to certain rules), though there's no reason a engine would actually need to implement it this way. Using await is the normal way ECMAScript consumes things which are expected to be Promises, and I would expect other consumers to follow suit unless there's specific reason to do something else, both here and also anywhere else they're consuming Promises.

domfarolino commented 4 weeks ago

OK great, this is a super useful response and great information. Thanks a lot! I'll close this.