Skip to content
Permalink
Browse files
[JSC] Implement array-from-async
https://bugs.webkit.org/show_bug.cgi?id=245260
rdar://problem/100303653

Reviewed by Alexey Shvayka.

This patch implements stage-3 Array.fromAsync[1] feature.
The goal of this feature is providing async iteration version of Array.from.

Array.from's concept.

    const arr = [];
    for (const v of iterable) {
        arr.push(v);
    }

Array.fromAsync's concept.

    const arr = [];
    for await (const v of asyncIterable) {
        arr.push(v);
    }

The complicated part of this change is that, when using `async function` in builtin JS,
it automatically generates internal promise, which we would like to avoid here.
In the future, we would like to remove internal promise completely, but for now, we
workaround this restriction by the convention that, when the builtin JS function's name
starts with `defaultAsync`, then we use normal promise instead of internal promise.

[1]: https://github.com/tc39/proposal-array-from-async

* JSTests/stress/array-from-async-basic.js: Added.
(shouldBe):
(shouldBeArray):
(async test.async shouldBeArray):
(async test):
(test.catch):
* JSTests/stress/array-from-async-map-promise.js: Added.
(shouldBe):
(shouldBeArray):
(async test.):
(async test.async shouldBeArray):
(async test):
(test.catch):
* JSTests/stress/array-from-async-map-this.js: Added.
(shouldBe):
(shouldBeArray):
(async test.):
(async test.async shouldBeArray):
(async test):
(test.catch):
* JSTests/stress/array-from-async-map.js: Added.
(shouldBe):
(shouldBeArray):
(async test.):
(async test.async shouldBeArray):
(async test):
(test.catch):
* Source/JavaScriptCore/builtins/ArrayConstructor.js:
(from.wrapper.iterator):
(from):
(wrapper.asyncIterator):
(linkTimeConstant.async defaultAsyncFromAsyncIterator):
(linkTimeConstant.async defaultAsyncFromAsyncArrayLike):
(fromAsync):
* Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp:
(JSC::BytecodeGenerator::BytecodeGenerator):
* Source/JavaScriptCore/runtime/ArrayConstructor.cpp:
(JSC::ArrayConstructor::finishCreation):
* Source/JavaScriptCore/runtime/OptionsList.h:

Canonical link: https://commits.webkit.org/257177@main
  • Loading branch information
Constellation committed Nov 30, 2022
1 parent 4467fe2 commit 464a308595c09e6151402adbb8e4e94018a115bd
Show file tree
Hide file tree
Showing 10 changed files with 464 additions and 6 deletions.
@@ -0,0 +1,147 @@
//@ requireOptions("--useArrayFromAsync=1")

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function shouldBeArray(actual, expected) {
shouldBe(actual.length, expected.length);
for (var i = 0; i < expected.length; ++i) {
try {
shouldBe(actual[i], expected[i]);
} catch(e) {
print(JSON.stringify(actual));
throw e;
}
}
}

shouldBe(Array.fromAsync.length, 1);

async function test()
{
// sync iterator
{
let result = await Array.fromAsync([0, 1, 2, 3, 4, 5]);
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}
{
let result = await Array.fromAsync(function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}());
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}

// async iterator
{
let result = await Array.fromAsync(async function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}());
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}

// array-like
{
let result = await Array.fromAsync({
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
});
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}

try {
await Array.fromAsync(null);
} catch (error) {
shouldBe(String(error), `TypeError: null is not an object`);
}

try {
await Array.fromAsync(undefined);
} catch (error) {
shouldBe(String(error), `TypeError: undefined is not an object`);
}

try {
await Array.fromAsync({ [Symbol.asyncIterator]: 42 });
} catch (error) {
shouldBe(String(error), `TypeError: Array.fromAsync requires that the property of the first argument, items[Symbol.asyncIterator], when exists, be a function`);
}

try {
await Array.fromAsync({ [Symbol.iterator]: 42 });
} catch (error) {
shouldBe(String(error), `TypeError: Array.fromAsync requires that the property of the first argument, items[Symbol.iterator], when exists, be a function`);
}

// array-like, asyncIterator is ignored.
{
let result = await Array.fromAsync({
[Symbol.asyncIterator]: null,
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
});
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}
{
let result = await Array.fromAsync({
[Symbol.asyncIterator]: undefined,
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
});
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}

// array-like, iterator is ignored.
{
let result = await Array.fromAsync({
[Symbol.iterator]: null,
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
});
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}
{
let result = await Array.fromAsync({
[Symbol.iterator]: undefined,
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
});
shouldBeArray(result, [0, 1, 2, 3, 4, 5]);
}
}

test().catch(function (error) {
print("FAIL");
print(String(error));
print(String(error.stack));
$vm.abort()
});
drainMicrotasks();
@@ -0,0 +1,67 @@
//@ requireOptions("--useArrayFromAsync=1")

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function shouldBeArray(actual, expected) {
shouldBe(actual.length, expected.length);
for (var i = 0; i < expected.length; ++i) {
try {
shouldBe(actual[i], expected[i]);
} catch(e) {
print(JSON.stringify(actual));
throw e;
}
}
}

shouldBe(Array.fromAsync.length, 1);

async function test()
{
// sync iterator
{
let result = await Array.fromAsync([0, 1, 2, 3, 4, 5], function (value) { return Promise.resolve(value * value) });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}
{
let result = await Array.fromAsync(function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}(), function (value) { return Promise.resolve(value * value) });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}

// async iterator
{
let result = await Array.fromAsync(async function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}(), function (value) { return Promise.resolve(value * value) });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}

// array-like
{
let result = await Array.fromAsync({
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
}, function (value) { return Promise.resolve(value * value) });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}
}

test().catch(function (error) {
print("FAIL");
print(String(error));
print(String(error.stack));
$vm.abort()
});
drainMicrotasks();
@@ -0,0 +1,67 @@
//@ requireOptions("--useArrayFromAsync=1")

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function shouldBeArray(actual, expected) {
shouldBe(actual.length, expected.length);
for (var i = 0; i < expected.length; ++i) {
try {
shouldBe(actual[i], expected[i]);
} catch(e) {
print(JSON.stringify(actual));
throw e;
}
}
}

shouldBe(Array.fromAsync.length, 1);

async function test()
{
// sync iterator
{
let result = await Array.fromAsync([0, 1, 2, 3, 4, 5], function (value) { return value * value + this }, 42);
shouldBeArray(result, [42, 43, 46, 51, 58, 67]);
}
{
let result = await Array.fromAsync(function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}(), function (value) { return value * value + this }, 42);
shouldBeArray(result, [42, 43, 46, 51, 58, 67]);
}

// async iterator
{
let result = await Array.fromAsync(async function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}(), function (value) { return value * value + this }, 42);
shouldBeArray(result, [42, 43, 46, 51, 58, 67]);
}

// array-like
{
let result = await Array.fromAsync({
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
}, function (value) { return value * value + this }, 42);
shouldBeArray(result, [42, 43, 46, 51, 58, 67]);
}
}

test().catch(function (error) {
print("FAIL");
print(String(error));
print(String(error.stack));
$vm.abort()
}, 42);
drainMicrotasks();
@@ -0,0 +1,67 @@
//@ requireOptions("--useArrayFromAsync=1")

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

function shouldBeArray(actual, expected) {
shouldBe(actual.length, expected.length);
for (var i = 0; i < expected.length; ++i) {
try {
shouldBe(actual[i], expected[i]);
} catch(e) {
print(JSON.stringify(actual));
throw e;
}
}
}

shouldBe(Array.fromAsync.length, 1);

async function test()
{
// sync iterator
{
let result = await Array.fromAsync([0, 1, 2, 3, 4, 5], function (value) { return value * value });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}
{
let result = await Array.fromAsync(function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}(), function (value) { return value * value });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}

// async iterator
{
let result = await Array.fromAsync(async function* generator() {
for (var i = 0; i < 6; ++i)
yield i;
}(), function (value) { return value * value });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}

// array-like
{
let result = await Array.fromAsync({
[0]: 0,
[1]: 1,
[2]: 2,
[3]: 3,
[4]: 4,
[5]: 5,
length: 6,
}, function (value) { return value * value });
shouldBeArray(result, [0, 1, 4, 9, 16, 25]);
}
}

test().catch(function (error) {
print("FAIL");
print(String(error));
print(String(error.stack));
$vm.abort()
});
drainMicrotasks();
@@ -46,7 +46,7 @@ PASS getSortedOwnPropertyNames(Object) is ['assign', 'create', 'defineProperties
PASS getSortedOwnPropertyNames(Object.prototype) is ['__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', '__proto__', 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf']
PASS getSortedOwnPropertyNames(Function) is ['length', 'name', 'prototype']
PASS getSortedOwnPropertyNames(Function.prototype) is ['apply', 'arguments', 'bind', 'call', 'caller', 'constructor', 'length', 'name', 'toString']
PASS getSortedOwnPropertyNames(Array) is ['from', 'isArray', 'length', 'name', 'of', 'prototype']
PASS getSortedOwnPropertyNames(Array) is ['from', 'fromAsync', 'isArray', 'length', 'name', 'of', 'prototype']
PASS getSortedOwnPropertyNames(Array.prototype) is ['at', 'concat', 'constructor', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'flat', 'flatMap', 'forEach', 'group', 'groupToMap', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'length', 'map', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice', 'toLocaleString', 'toReversed', 'toSorted', 'toSpliced', 'toString', 'unshift', 'values', 'with']
PASS getSortedOwnPropertyNames(String) is ['fromCharCode', 'fromCodePoint', 'length', 'name', 'prototype', 'raw']
PASS getSortedOwnPropertyNames(String.prototype) is ['anchor', 'at', 'big', 'blink', 'bold', 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'constructor', 'endsWith', 'fixed', 'fontcolor', 'fontsize', 'includes', 'indexOf', 'italics', 'lastIndexOf', 'length', 'link', 'localeCompare', 'match', 'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'replaceAll', 'search', 'slice', 'small', 'split', 'startsWith', 'strike', 'sub', 'substr', 'substring', 'sup', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimLeft', 'trimRight', 'trimStart', 'valueOf']
@@ -55,7 +55,7 @@ var expectedPropertyNamesSet = {
"Object.prototype": "['__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', '__proto__', 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf']",
"Function": "['length', 'name', 'prototype']",
"Function.prototype": "['apply', 'arguments', 'bind', 'call', 'caller', 'constructor', 'length', 'name', 'toString']",
"Array": "['from', 'isArray', 'length', 'name', 'of', 'prototype']",
"Array": "['from', 'fromAsync', 'isArray', 'length', 'name', 'of', 'prototype']",
"Array.prototype": "['at', 'concat', 'constructor', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'flat', 'flatMap', 'forEach', 'group', 'groupToMap', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'length', 'map', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice', 'toLocaleString', 'toReversed', 'toSorted', 'toSpliced', 'toString', 'unshift', 'values', 'with']",
"String": "['fromCharCode', 'fromCodePoint', 'length', 'name', 'prototype', 'raw']",
"String.prototype": "['anchor', 'at', 'big', 'blink', 'bold', 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'constructor', 'endsWith', 'fixed', 'fontcolor', 'fontsize', 'includes', 'indexOf', 'italics', 'lastIndexOf', 'length', 'link', 'localeCompare', 'match', 'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'replaceAll', 'search', 'slice', 'small', 'split', 'startsWith', 'strike', 'sub', 'substr', 'substring', 'sup', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimLeft', 'trimRight', 'trimStart', 'valueOf']",

0 comments on commit 464a308

Please sign in to comment.