LibJS: Replace Array.fromAsync with a native JavaScript implementation

This allows us to use the bytecode implementation of await, which
correctly suspends execution contexts and handles completion
injections.

This gains us 4 test262 tests around mutating Array.fromAsync's
iterable whilst it's suspended as well.

This is also one step towards removing spin_until, which the
non-bytecode implementation of await uses.

```
Duration:
     -5.98s

Summary:
    Diff Tests:
        +4     -4 

Diff Tests:
    [...]/Array/fromAsync/asyncitems-array-add-to-singleton.js  -> 
    [...]/Array/fromAsync/asyncitems-array-add.js               -> 
    [...]/Array/fromAsync/asyncitems-array-mutate.js            -> 
    [...]/Array/fromAsync/asyncitems-array-remove.js            -> 
```
This commit is contained in:
Luke Wilde 2025-11-06 19:20:29 +00:00 committed by Andreas Kling
parent a63b0cfaba
commit 0eceee0a05
Notes: github-actions[bot] 2025-11-30 10:56:04 +00:00
15 changed files with 942 additions and 233 deletions

View file

@ -0,0 +1,95 @@
/**
* 7.3.10 GetMethod ( V, P ), https://tc39.es/ecma262/#sec-getmethod
*/
function GetMethod(value, property) {
// 1. Let func be ? GetV(V, P).
const function_ = value[property];
// 2. If func is either undefined or null, return undefined.
if (function_ === undefined || function_ === null) return undefined;
// 3. If IsCallable(func) is false, throw a TypeError exception.
if (!IsCallable(function_)) ThrowTypeError("Not a function");
// 4. Return func.
return function_;
}
/**
* 7.4.2 GetIteratorDirect ( obj ), https://tc39.es/ecma262/#sec-getiteratordirect
*/
function GetIteratorDirect(object) {
// 1. Let nextMethod be ? Get(obj, "next").
const nextMethod = object.next;
// 2. Let iteratorRecord be the Iterator Record { [[Iterator]]: obj, [[NextMethod]]: nextMethod, [[Done]]: false }.
const iteratorRecord = NewObjectWithNoPrototype();
iteratorRecord.iterator = object;
iteratorRecord.nextMethod = nextMethod;
iteratorRecord.done = false;
// 3. Return iteratorRecord.
return iteratorRecord;
}
/**
* 7.4.3 GetIteratorFromMethod ( obj, method ), https://tc39.es/ecma262/#sec-getiteratorfrommethod
*/
function GetIteratorFromMethod(object, method) {
// 1. Let iterator be ? Call(method, obj).
const iterator = Call(method, object);
// 2. If iterator is not an Object, throw a TypeError exception.
ThrowIfNotObject(iterator);
// 3. Return ? GetIteratorDirect(iterator).
return GetIteratorDirect(iterator);
}
/**
* 7.4.7 IteratorComplete ( iteratorResult ) - https://tc39.es/ecma262/#sec-iteratorcomplete
*/
function IteratorComplete(iteratorResult) {
// 1. Return ToBoolean(? Get(iteratorResult, "done")).
return ToBoolean(iteratorResult.done);
}
/**
* 7.4.13 AsyncIteratorClose ( iteratorRecord, completion ) - https://tc39.es/ecma262/#sec-asynciteratorclose
*/
async function AsyncIteratorClose(iteratorRecord, completionValue, isThrowCompletion) {
// FIXME: 1. Assert: iteratorRecord.[[Iterator]] is an Object.
// 2. Let iterator be iteratorRecord.[[Iterator]].
const iterator = iteratorRecord.iterator;
let innerResult;
try {
// 3. Let innerResult be Completion(GetMethod(iterator, "return")).
innerResult = GetMethod(iterator, "return");
// 4. If innerResult is a normal completion, then
// a. Let return be innerResult.[[Value]].
// b. If return is undefined, return ? completion.
// NOTE: If isThrowCompletion is true, it will override this return in the finally block.
if (innerResult === undefined) return completionValue;
// c. Set innerResult to Completion(Call(return, iterator)).
// d. If innerResult is a normal completion, set innerResult to Completion(Await(innerResult.[[Value]])).
innerResult = await Call(innerResult, iterator);
} finally {
// 5. If completion is a throw completion, return ? completion.
if (isThrowCompletion) throw completionValue;
// 6. If innerResult is a throw completion, return ? innerResult.
// NOTE: If the try block threw, it will rethrow when leaving this finally block.
}
// 7. If innerResult.[[Value]] is not an Object, throw a TypeError exception.
ThrowIfNotObject(innerResult);
// 8. Return ? completion.
// NOTE: Because of step 5, this will not be a throw completion.
return completionValue;
}

View file

@ -0,0 +1,189 @@
/**
* 2.1.1.1 Array.fromAsync ( asyncItems [ , mapper [ , thisArg ] ] ), https://tc39.es/proposal-array-from-async/#sec-array.fromAsync
*/
async function fromAsync(asyncItems, mapper, thisArg) {
// 1. Let C be the this value.
const constructor = this;
let mapping;
// 2. If mapper is undefined, then
if (mapper === undefined) {
// a. Let mapping be false.
mapping = false;
}
// 3. Else,
else {
// a. If IsCallable(mapper) is false, throw a TypeError exception.
if (!IsCallable(mapper)) {
ThrowTypeError("mapper must be a function");
}
// b. Let mapping be true.
mapping = true;
}
// 4. Let usingAsyncIterator be ? GetMethod(asyncItems, %Symbol.asyncIterator%).
const usingAsyncIterator = GetMethod(asyncItems, SYMBOL_ASYNC_ITERATOR);
let usingSyncIterator = undefined;
// 5. If usingAsyncIterator is undefined, then
if (usingAsyncIterator === undefined) {
// a. Let usingSyncIterator be ? GetMethod(asyncItems, %Symbol.iterator%).
usingSyncIterator = GetMethod(asyncItems, SYMBOL_ITERATOR);
}
// 6. Let iteratorRecord be undefined.
let iteratorRecord = undefined;
// 7. If usingAsyncIterator is not undefined, then
if (usingAsyncIterator !== undefined) {
// a. Set iteratorRecord to ? GetIteratorFromMethod(asyncItems, usingAsyncIterator).
iteratorRecord = GetIteratorFromMethod(asyncItems, usingAsyncIterator);
}
// 8. Else if usingSyncIterator is not undefined, then
else if (usingSyncIterator !== undefined) {
// a. Set iteratorRecord to CreateAsyncFromSyncIterator(? GetIteratorFromMethod(asyncItems, usingSyncIterator)).
const iteratorFromMethod = GetIteratorFromMethod(asyncItems, usingSyncIterator);
iteratorRecord = CreateAsyncFromSyncIterator(
iteratorFromMethod.iterator,
iteratorFromMethod.nextMethod,
iteratorFromMethod.done
);
}
// 9. If iteratorRecord is not undefined, then
if (iteratorRecord !== undefined) {
let array;
// a. If IsConstructor(C) is true, then
if (IsConstructor(constructor)) {
// i. Let A be ? Construct(C).
array = new constructor();
}
// b. Else,
else {
// i. Let A be ! ArrayCreate(0).
array = [];
}
// c. Let k be 0.
// d. Repeat,
for (let k = 0; ; ++k) {
// i. If k ≥ 2**53 - 1, then
if (k >= MAX_ARRAY_LIKE_INDEX) {
// 1. Let error be ThrowCompletion(a newly created TypeError object).
const error = NewTypeError("Maximum array size exceeded");
// 2. Return ? AsyncIteratorClose(iteratorRecord, error).
return AsyncIteratorClose(iteratorRecord, error, true);
}
// ii. Let Pk be ! ToString(𝔽(k)).
// iii. Let nextResult be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
// iv. Set nextResult to ? Await(nextResult).
const nextResult = await Call(iteratorRecord.nextMethod, iteratorRecord.iterator);
// v. If nextResult is not an Object, throw a TypeError exception.
ThrowIfNotObject(nextResult);
// vi. Let done be ? IteratorComplete(nextResult).
const done = IteratorComplete(nextResult);
// vii. If done is true, then
if (done) {
// 1. Perform ? Set(A, "length", 𝔽(k), true).
array.length = k;
// 2. Return A.
return array;
}
// viii. Let nextValue be ? IteratorValue(nextResult).
const nextValue = nextResult.value;
try {
let mappedValue;
// ix. If mapping is true, then
if (mapping) {
// 1. Let mappedValue be Completion(Call(mapper, thisArg, « nextValue, 𝔽(k) »)).
// 2. IfAbruptCloseAsyncIterator(mappedValue, iteratorRecord).
// 3. Set mappedValue to Completion(Await(mappedValue)).
// 4. IfAbruptCloseAsyncIterator(mappedValue, iteratorRecord).
mappedValue = await Call(mapper, thisArg, nextValue, k);
}
// x. Else,
else {
// 1. Let mappedValue be nextValue.
mappedValue = nextValue;
}
// xi. Let defineStatus be Completion(CreateDataPropertyOrThrow(A, Pk, mappedValue)).
// xii. IfAbruptCloseAsyncIterator(defineStatus, iteratorRecord).
CreateDataPropertyOrThrow(array, k, mappedValue);
} catch (exception) {
return AsyncIteratorClose(iteratorRecord, exception, true);
}
// xiii. Set k to k + 1.
}
}
// 10. Else,
else {
// a. NOTE: asyncItems is neither an AsyncIterable nor an Iterable so assume it is an array-like object.
// b. Let arrayLike be ! ToObject(asyncItems).
const arrayLike = ToObject(asyncItems);
// c. Let len be ? LengthOfArrayLike(arrayLike).
const length = ToLength(arrayLike.length);
let array;
// d. If IsConstructor(C) is true, then
if (IsConstructor(constructor)) {
// i. Let A be ? Construct(C, « 𝔽(len) »).
array = new constructor(length);
}
// e. Else,
else {
// i. Let A be ? ArrayCreate(len).
array = NewArrayWithLength(length);
}
// f. Let k be 0.
// g. Repeat, while k < len,
for (let k = 0; k < length; ++k) {
// i. Let Pk be ! ToString(𝔽(k)).
// ii. Let kValue be ? Get(arrayLike, Pk).
// iii. Set kValue to ? Await(kValue).
const kValue = await arrayLike[k];
let mappedValue;
// iv. If mapping is true, then
if (mapping) {
// 1. Let mappedValue be ? Call(mapper, thisArg, « kValue, 𝔽(k) »).
// 2. Set mappedValue to ? Await(mappedValue).
mappedValue = await Call(mapper, thisArg, kValue, k);
}
// v. Else,
else {
// 1. Let mappedValue be kValue.
mappedValue = kValue;
}
// vi. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
CreateDataPropertyOrThrow(array, k, mappedValue);
// vii. Set k to k + 1.
}
// h. Perform ? Set(A, "length", 𝔽(len), true).
array.length = length;
// i. Return A.
return array;
}
}

View file

@ -0,0 +1,98 @@
/**
* Returns if the given value is callable (i.e. is a function)
* @param value {any} Value to check is callable
* @returns true if callable, false otherwise
*/
declare function IsCallable(value: any): boolean;
/**
* Returns if the given value is a constructor
* @param value {any} Value to check is a constructor
* @returns true if is a constructor, false otherwise
*/
declare function IsConstructor(value: any): boolean;
/**
* Converts the given value to its object form if it's not already an object.
* @param value {any} Value to convert to an object
* @returns {@link value} in its object form
* @throws {TypeError} If {@link value} is null or undefined
*/
declare function ToObject(value: any): object;
/**
* Converts the given value to a boolean.
* @param value {any} Value to convert to a boolean
* @returns {@link value} Value converted to a boolean
*/
declare function ToBoolean(value: any): boolean;
/**
* Converts the given value to a length.
* @param value {any} Value to convert to a length
* @returns {@link value} Value converted to a length
*/
declare function ToLength(value: any): number;
/**
* Throws a {@link TypeError} with the given message.
* @param message The reason the error was thrown.
* @throws {TypeError} With the given message.
*/
declare function ThrowTypeError(message: string): never;
/**
* Throws if the given value is an object.
* @param value {any} Value to check is an object
* @throws {TypeError} If value is not an object
*/
declare function ThrowIfNotObject(value: any): void;
/**
* Creates a new object with no properties and no prototype.
*/
declare function NewObjectWithNoPrototype(): object;
/**
* Creates a new TypeError with the given message.
*/
declare function NewTypeError(message: string): TypeError;
/**
* Creates a new array with a starting length.
*/
declare function NewArrayWithLength(length: number): Array<any>;
/**
* Creates an AsyncFromSyncIterator object.
*/
declare function CreateAsyncFromSyncIterator(iterator: object, nextMethod: any, done: boolean): object;
/**
* Creates the given property on the given object with the given value.
*/
declare function CreateDataPropertyOrThrow(object: object, property: string | number, value: any): undefined;
/**
* Calls the given callee with `this` set to `thisValue` and with the passed in arguments.
* @param callee The function to call
* @param thisValue The value of `this` to use
* @param args The arguments to pass to the function
* @returns The result of calling the function
*/
declare function Call(callee: any, thisValue: any, ...args: any[]): any;
/**
* @defaultValue {@link Symbol.iterator}
*/
declare const SYMBOL_ITERATOR: symbol;
/**
* @defaultValue {@link Symbol.asyncIterator}
*/
declare const SYMBOL_ASYNC_ITERATOR: symbol;
/**
* @defaultValue 2 ** 53 - 1
*/
declare const MAX_ARRAY_LIKE_INDEX: number;