Ecma262: Why does AsyncFromSyncIterator exist?

Created on 30 Oct 2019  ·  5Comments  ·  Source: tc39/ecma262

AsyncFromSyncIterator is a spec fiction converts sync iterators into async iterators, i.e., iterators which return promises. It is not accessible to ECMAScript code (as of #1474).

It is used in two places: in for-await-of loops and in yield* in async generators. Specifically, it is used when those constructs are given objects which have Symbol.iterator but not Symbol.asyncIterator.

But both of those places always Await the result of the NextMethod of the iterator they're working with, and that macro wraps its argument in a Promise anyway. Indeed, if you make an object which has a _sync_ iterator in its Symbol.asyncIterator property, that will work fine:

let asyncArray = [0, 1];
asyncArray[Symbol.asyncIterator] = Array.prototype[Symbol.iterator];

// this logs `0 false`, `1 false`, and `undefined true`

async function* yieldstar() {
  yield* asyncArray;
}
let iter = yieldstar();
iter.next().then(a => { console.log(a.value, a.done); });
iter.next().then(a => { console.log(a.value, a.done); });
iter.next().then(a => { console.log(a.value, a.done); });


// this logs `0` and `1`

async function forawait() {
  for await (let value of asyncArray) {
    console.log(value);
  }
}
forawait();

As far as I can tell, the only only observable difference made by wrapping is that it causes there to be additional PromiseJob queue items (although it seems there is not broad agreement among the engines about exactly how many).

So what's the point of wrapping the sync iterator? Is it just to cause these additional queue items?

question

All 5 comments

When you say “not broad agreement”, can you provide some more detailed comparisons?

Sure, here's a gist.

Roughly speaking, JSC and V8 have 1 additional queue item per step, SpiderMonkey has 2, and XS has 0 (i.e., it behaves as if it does no wrapping at all).

QJS also has one tick, and the iterator proposal is planning to use AsyncFromSyncIteratorPrototype so that you don't have to worry about whether or not you've wrapped a sync iterator when you do AsyncIterator.from().

It has the additional feature of unwrapping yielded promises from synchronous iterators:

function *foo() {
    for (let i = 1; i <= 3; i++) {
        yield Promise.resolve(i);
    }
}

// This logs: Promise { 1 }, Promise { 2 }, Promise { 3 }
function forOfFoo() {
    for (let value of foo()) {
        console.log(value);
    }
}
forOfFoo();

// This logs: 1, 2, 3
async function forAwaitOfFoo() {
    for await (let value of foo()) {
        console.log(value);
    }
}
forAwaitOfFoo();

To demonstrate what @ExE-Boss is saying, consider

function* g(){
  yield 0; yield Promise.resolve(1);
}

(async function() {
  for await (let value of g()) {
    console.log(value);
  }
})();

This prints 0, 1.

But if instead we bypassed AsyncFromSyncIIterator by doing

(async function() {
  for await (let value of { [Symbol.asyncIterator]: g }) {
    console.log(value);
  }
})();

we would get 0, Promise { 1 }.

Presumably that's why this exists. I'll go ahead and close this.

Was this page helpful?
0 / 5 - 0 ratings