diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json
index 073635a7b..364816748 100644
--- a/Jint.Tests.Test262/Test262Harness.settings.json
+++ b/Jint.Tests.Test262/Test262Harness.settings.json
@@ -5,7 +5,6 @@
"Namespace": "Jint.Tests.Test262",
"Parallel": true,
"ExcludedFeatures": [
- "Array.fromAsync",
"Atomics",
"decorators",
"import-defer",
diff --git a/Jint/Engine.cs b/Jint/Engine.cs
index 3d35897a6..722699588 100644
--- a/Jint/Engine.cs
+++ b/Jint/Engine.cs
@@ -1,5 +1,4 @@
using System.Diagnostics;
-using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Jint.Collections;
diff --git a/Jint/Native/Array/ArrayConstructor.cs b/Jint/Native/Array/ArrayConstructor.cs
index 5306f2515..17beb2e7b 100644
--- a/Jint/Native/Array/ArrayConstructor.cs
+++ b/Jint/Native/Array/ArrayConstructor.cs
@@ -2,8 +2,10 @@
using System.Collections;
using Jint.Native.Function;
+using Jint.Native.Generator;
using Jint.Native.Iterator;
using Jint.Native.Object;
+using Jint.Native.Promise;
using Jint.Native.Symbol;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
@@ -32,9 +34,10 @@ internal ArrayConstructor(
protected override void Initialize()
{
- var properties = new PropertyDictionary(3, checkExistingKeys: false)
+ var properties = new PropertyDictionary(4, checkExistingKeys: false)
{
["from"] = new PropertyDescriptor(new PropertyDescriptor(new ClrFunction(Engine, "from", From, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable)),
+ ["fromAsync"] = new PropertyDescriptor(new PropertyDescriptor(new ClrFunction(Engine, "fromAsync", FromAsync, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable)),
["isArray"] = new PropertyDescriptor(new PropertyDescriptor(new ClrFunction(Engine, "isArray", IsArray, 1), PropertyFlag.NonEnumerable)),
["of"] = new PropertyDescriptor(new PropertyDescriptor(new ClrFunction(Engine, "of", Of, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable))
};
@@ -90,6 +93,407 @@ private JsValue From(JsValue thisObject, JsCallArguments arguments)
return ConstructArrayFromArrayLike(thisObject, source, callable, thisArg);
}
+ ///
+ /// https://tc39.es/ecma262/#sec-array.fromasync
+ ///
+ private JsValue FromAsync(JsValue thisObject, JsCallArguments arguments)
+ {
+ var asyncItems = arguments.At(0);
+ var mapfn = arguments.At(1);
+ var thisArg = arguments.At(2);
+
+ // 1. Let C be the this value.
+ var c = thisObject;
+
+ // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
+ var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise);
+
+ // 3. Let fromAsyncClosure be a new Abstract Closure with no parameters that captures C, mapfn, and thisArg
+ // and performs the following steps when called:
+ // 4. Perform ! Call(fromAsyncClosure, undefined).
+ // The closure is called synchronously per spec (step 4)
+ FromAsyncClosure(c, asyncItems, mapfn, thisArg, promiseCapability);
+
+ // 5. Return promiseCapability.[[Promise]].
+ return promiseCapability.PromiseInstance;
+ }
+
+ private void FromAsyncClosure(JsValue c, JsValue asyncItems, JsValue mapfn, JsValue thisArg, PromiseCapability promiseCapability)
+ {
+ try
+ {
+ FromAsyncClosureImpl(c, asyncItems, mapfn, thisArg, promiseCapability);
+ }
+ catch (JavaScriptException e)
+ {
+ promiseCapability.Reject.Call(Undefined, e.Error);
+ }
+ }
+
+ private void FromAsyncClosureImpl(JsValue c, JsValue asyncItems, JsValue mapfn, JsValue thisArg, PromiseCapability promiseCapability)
+ {
+ // a. If mapfn is undefined, let mapping be false.
+ // b. Else,
+ // i. If IsCallable(mapfn) is false, throw a TypeError exception.
+ // ii. Let mapping be true.
+ ICallable? callable = null;
+ if (!mapfn.IsUndefined())
+ {
+ if (mapfn is not ICallable mapCallable)
+ {
+ Throw.TypeError(_realm, $"{mapfn} is not a function");
+ return;
+ }
+ callable = mapCallable;
+ }
+
+ // c. Let usingAsyncIterator be ? GetMethod(asyncItems, @@asyncIterator).
+ if (asyncItems.IsNullOrUndefined())
+ {
+ Throw.TypeError(_realm, "Cannot convert undefined or null to object");
+ return;
+ }
+
+ var usingAsyncIterator = GetMethod(_realm, asyncItems, GlobalSymbolRegistry.AsyncIterator);
+
+ // d. If usingAsyncIterator is undefined, then
+ // i. Let usingSyncIterator be ? GetMethod(asyncItems, @@iterator).
+ ICallable? usingSyncIterator = null;
+ if (usingAsyncIterator is null)
+ {
+ usingSyncIterator = GetMethod(_realm, asyncItems, GlobalSymbolRegistry.Iterator);
+ }
+
+ // e. If usingAsyncIterator is not undefined or usingSyncIterator is not undefined, then
+ if (usingAsyncIterator is not null || usingSyncIterator is not null)
+ {
+ FromAsyncWithIterator(c, asyncItems, callable, thisArg, promiseCapability, usingAsyncIterator, usingSyncIterator);
+ }
+ else
+ {
+ // f. Else,
+ // i. NOTE: asyncItems is neither an AsyncIterable nor an Iterable so assume it is an array-like object.
+ FromAsyncWithArrayLike(c, asyncItems, callable, thisArg, promiseCapability);
+ }
+ }
+
+ private void FromAsyncWithIterator(
+ JsValue c,
+ JsValue asyncItems,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability,
+ ICallable? usingAsyncIterator,
+ ICallable? usingSyncIterator)
+ {
+ // ii. Let iteratorRecord be ? GetIterator(asyncItems, async).
+ // We'll handle both async and sync iterators
+ ObjectInstance instance;
+ if (!ReferenceEquals(this, c) && c is IConstructor constructor)
+ {
+ instance = constructor.Construct([], c);
+ }
+ else
+ {
+ instance = ArrayCreate(0);
+ }
+
+ var target = ArrayOperations.For(instance, forWrite: true);
+
+ if (usingAsyncIterator is not null)
+ {
+ // Use async iterator directly - GetIterator with Async hint handles the wrapping
+ var iterator = asyncItems.GetIterator(_realm, GeneratorKind.Async, usingAsyncIterator);
+ FromAsyncIteratorLoop(iterator.Instance, target, callable, thisArg, promiseCapability, 0);
+ }
+ else if (usingSyncIterator is not null)
+ {
+ // Wrap sync iterator in async wrapper
+ var syncIterator = asyncItems.GetIterator(_realm, GeneratorKind.Sync, usingSyncIterator);
+ var asyncIterator = new AsyncFromSyncIterator(_engine, syncIterator);
+ FromAsyncIteratorLoop(asyncIterator, target, callable, thisArg, promiseCapability, 0);
+ }
+ }
+
+ private void FromAsyncIteratorLoop(
+ ObjectInstance iterator,
+ ArrayOperations target,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability,
+ ulong k)
+ {
+ try
+ {
+ // Get next method and call it
+ var nextMethod = iterator.Get(CommonProperties.Next);
+ if (nextMethod is not ICallable nextCallable)
+ {
+ promiseCapability.Reject.Call(Undefined, _realm.Intrinsics.TypeError.Construct("Iterator next is not a function"));
+ return;
+ }
+
+ var next = nextCallable.Call(iterator, Arguments.Empty);
+
+ // The result should be a promise (or wrap it in one)
+ var promiseResolve = _realm.Intrinsics.Promise.PromiseResolve(next);
+
+ var capturedK = k;
+ var onFulfilled = new ClrFunction(_engine, "", (_, args) =>
+ {
+ var iterResult = args.At(0);
+ if (iterResult is not ObjectInstance iterResultObj)
+ {
+ promiseCapability.Reject.Call(Undefined, _realm.Intrinsics.TypeError.Construct("Iterator result is not an object"));
+ return Undefined;
+ }
+
+ var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done));
+ var value = iterResultObj.Get(CommonProperties.Value);
+ if (done)
+ {
+ // Iteration complete
+ target.SetLength(capturedK);
+ promiseCapability.Resolve.Call(Undefined, target.Target);
+ return Undefined;
+ }
+
+ ProcessAsyncIteratorValue(value, target, callable, thisArg, promiseCapability, capturedK, () =>
+ {
+ // Queue next iteration to prevent stack overflow
+ _engine.AddToEventLoop(() => FromAsyncIteratorLoop(iterator, target, callable, thisArg, promiseCapability, capturedK + 1));
+ });
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ var onRejected = new ClrFunction(_engine, "", (_, args) =>
+ {
+ promiseCapability.Reject.Call(Undefined, args);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ PromiseOperations.PerformPromiseThen(_engine, (JsPromise) promiseResolve, onFulfilled, onRejected, null!);
+ }
+ catch (JavaScriptException e)
+ {
+ promiseCapability.Reject.Call(Undefined, e.Error);
+ }
+ }
+
+ private void ProcessAsyncIteratorValue(
+ JsValue value,
+ ArrayOperations target,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability,
+ ulong k,
+ Action continueLoop)
+ {
+ // Await the value
+ var valuePromise = _realm.Intrinsics.Promise.PromiseResolve(value);
+
+ var onFulfilled = new ClrFunction(_engine, "", (_, args) =>
+ {
+ var resolvedValue = args.At(0);
+ ProcessMappedValue(resolvedValue, target, callable, thisArg, promiseCapability, k, continueLoop);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ var onRejected = new ClrFunction(_engine, "", (_, args) =>
+ {
+ promiseCapability.Reject.Call(Undefined, args);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ PromiseOperations.PerformPromiseThen(_engine, (JsPromise) valuePromise, onFulfilled, onRejected, null!);
+ }
+
+ private void ProcessMappedValue(
+ JsValue value,
+ ArrayOperations target,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability,
+ ulong k,
+ Action continueLoop)
+ {
+ try
+ {
+ if (callable is not null)
+ {
+ // Apply mapfn
+ var mappedValue = callable.Call(thisArg, value, k);
+
+ // Await the mapped value
+ var mappedPromise = _realm.Intrinsics.Promise.PromiseResolve(mappedValue);
+
+ var onFulfilled = new ClrFunction(_engine, "", (_, args) =>
+ {
+ var resolvedMapped = args.At(0);
+ target.CreateDataPropertyOrThrow(k, resolvedMapped);
+ // Queue continuation to prevent stack overflow
+ _engine.AddToEventLoop(continueLoop);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ var onRejected = new ClrFunction(_engine, "", (_, args) =>
+ {
+ promiseCapability.Reject.Call(Undefined, args);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ PromiseOperations.PerformPromiseThen(_engine, (JsPromise) mappedPromise, onFulfilled, onRejected, null!);
+ }
+ else
+ {
+ target.CreateDataPropertyOrThrow(k, value);
+ // Queue continuation to prevent stack overflow
+ _engine.AddToEventLoop(continueLoop);
+ }
+ }
+ catch (JavaScriptException e)
+ {
+ promiseCapability.Reject.Call(Undefined, e.Error);
+ }
+ }
+
+ private void FromAsyncWithArrayLike(
+ JsValue c,
+ JsValue asyncItems,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability)
+ {
+ try
+ {
+ // ii. Let arrayLike be ! ToObject(asyncItems).
+ var arrayLike = TypeConverter.ToObject(_realm, asyncItems);
+
+ // iii. Let len be ? LengthOfArrayLike(arrayLike).
+ var len = arrayLike.GetLength();
+
+ // iv. If IsConstructor(C) is true, then
+ // 1. Let A be ? Construct(C, « 𝔽(len) »).
+ // v. Else,
+ // 1. Let A be ? ArrayCreate(len).
+ ObjectInstance a;
+ if (!ReferenceEquals(c, this) && c is IConstructor constructor)
+ {
+ a = constructor.Construct([len], c);
+ }
+ else
+ {
+ a = ArrayCreate(len);
+ }
+
+ var target = ArrayOperations.For(a, forWrite: true);
+ FromAsyncArrayLikeLoop(arrayLike, target, callable, thisArg, promiseCapability, 0, len);
+ }
+ catch (JavaScriptException e)
+ {
+ promiseCapability.Reject.Call(Undefined, e.Error);
+ }
+ }
+
+ private void FromAsyncArrayLikeLoop(
+ ObjectInstance arrayLike,
+ ArrayOperations target,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability,
+ ulong k,
+ ulong len)
+ {
+ if (k >= len)
+ {
+ // Done
+ target.SetLength(len);
+ promiseCapability.Resolve.Call(Undefined, target.Target);
+ return;
+ }
+
+ try
+ {
+ // Get the value at k
+ var pk = k;
+ var kValue = arrayLike.Get(pk);
+
+ // Await the value
+ var valuePromise = _realm.Intrinsics.Promise.PromiseResolve(kValue);
+
+ var capturedK = k;
+ var onFulfilled = new ClrFunction(_engine, "", (_, args) =>
+ {
+ var resolvedValue = args.At(0);
+ ProcessArrayLikeMappedValue(arrayLike, resolvedValue, target, callable, thisArg, promiseCapability, capturedK, len);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ var onRejected = new ClrFunction(_engine, "", (_, args) =>
+ {
+ promiseCapability.Reject.Call(Undefined, args);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ PromiseOperations.PerformPromiseThen(_engine, (JsPromise) valuePromise, onFulfilled, onRejected, null!);
+ }
+ catch (JavaScriptException e)
+ {
+ promiseCapability.Reject.Call(Undefined, e.Error);
+ }
+ }
+
+ private void ProcessArrayLikeMappedValue(
+ ObjectInstance arrayLike,
+ JsValue value,
+ ArrayOperations target,
+ ICallable? callable,
+ JsValue thisArg,
+ PromiseCapability promiseCapability,
+ ulong k,
+ ulong len)
+ {
+ try
+ {
+ if (callable is not null)
+ {
+ // Apply mapfn
+ var mappedValue = callable.Call(thisArg, value, k);
+
+ // Await the mapped value
+ var mappedPromise = _realm.Intrinsics.Promise.PromiseResolve(mappedValue);
+
+ var capturedK = k;
+ var onFulfilled = new ClrFunction(_engine, "", (_, args) =>
+ {
+ var resolvedMapped = args.At(0);
+ target.CreateDataPropertyOrThrow(capturedK, resolvedMapped);
+ // Queue next iteration to prevent stack overflow
+ _engine.AddToEventLoop(() => FromAsyncArrayLikeLoop(arrayLike, target, callable, thisArg, promiseCapability, capturedK + 1, len));
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ var onRejected = new ClrFunction(_engine, "", (_, args) =>
+ {
+ promiseCapability.Reject.Call(Undefined, args);
+ return Undefined;
+ }, 1, PropertyFlag.Configurable);
+
+ PromiseOperations.PerformPromiseThen(_engine, (JsPromise) mappedPromise, onFulfilled, onRejected, null!);
+ }
+ else
+ {
+ target.CreateDataPropertyOrThrow(k, value);
+ // Queue next iteration to prevent stack overflow
+ _engine.AddToEventLoop(() => FromAsyncArrayLikeLoop(arrayLike, target, callable, thisArg, promiseCapability, k + 1, len));
+ }
+ }
+ catch (JavaScriptException e)
+ {
+ promiseCapability.Reject.Call(Undefined, e.Error);
+ }
+ }
+
private ObjectInstance ConstructArrayFromArrayLike(
JsValue thisObj,
ArrayOperations source,
diff --git a/Jint/Native/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs b/Jint/Native/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs
index c86ad4fd0..3fc38298e 100644
--- a/Jint/Native/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs
+++ b/Jint/Native/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs
@@ -1,5 +1,4 @@
-using Jint.Native.AsyncFunction;
-using Jint.Native.Function;
+using Jint.Native.Function;
using Jint.Native.Iterator;
using Jint.Native.Object;
using Jint.Runtime;
diff --git a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs
index 768cda9a5..25f6e88f4 100644
--- a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs
+++ b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs
@@ -5,7 +5,6 @@
using Jint.Native.AsyncGenerator;
using Jint.Native.Generator;
using Jint.Native.Promise;
-using Jint.Runtime.Environments;
using Jint.Runtime.Interpreter.Expressions;
namespace Jint.Runtime.Interpreter;
diff --git a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs
index a80a7fd50..429bba807 100644
--- a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs
@@ -70,12 +70,17 @@ protected override Completion ExecuteInternal(EvaluationContext context)
// If resuming from init expression, we must re-execute init to complete pending nested awaits
var generator = engine.ExecutionContext.Generator;
var asyncFn = engine.ExecutionContext.AsyncFunction;
+ var asyncGenerator = engine.ExecutionContext.AsyncGenerator;
Node? resumeNode = null;
if (generator is not null && generator._isResuming && generator._lastYieldNode is Node yieldNode)
{
resumeNode = yieldNode;
}
+ else if (asyncGenerator is not null && asyncGenerator._isResuming && asyncGenerator._lastYieldNode is Node asyncYieldNode)
+ {
+ resumeNode = asyncYieldNode;
+ }
else if (asyncFn is not null && asyncFn._isResuming && asyncFn._lastAwaitNode is JintExpression awaitExpr
&& awaitExpr._expression is Node awaitNode)
{
@@ -92,6 +97,10 @@ protected override Completion ExecuteInternal(EvaluationContext context)
{
generator.TryGetSuspendData(this, out suspendData);
}
+ else if (asyncGenerator is not null)
+ {
+ asyncGenerator.TryGetSuspendData(this, out suspendData);
+ }
else if (asyncFn is not null)
{
asyncFn.TryGetSuspendData(this, out suspendData);
@@ -183,6 +192,17 @@ protected override Completion ExecuteInternal(EvaluationContext context)
data.BoundValues[name] = value;
}
}
+ else if (asyncGenerator is not null)
+ {
+ var data = asyncGenerator.GetOrCreateSuspendData(this);
+ data.BoundValues ??= new Dictionary();
+ for (var i = 0; i < _boundNames.Count; i++)
+ {
+ var name = _boundNames[i];
+ var value = currentEnv.GetBindingValue(name, strict: false);
+ data.BoundValues[name] = value;
+ }
+ }
else if (asyncFn is not null)
{
var data = asyncFn.GetOrCreateSuspendData(this);
@@ -199,6 +219,7 @@ protected override Completion ExecuteInternal(EvaluationContext context)
{
// Clear suspend data on normal completion
generator?.ClearSuspendData(this);
+ asyncGenerator?.ClearSuspendData(this);
asyncFn?.ClearSuspendData(this);
}
diff --git a/README.md b/README.md
index 68821a50b..ca5b162d5 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ and many more.
#### ECMAScript 2025
- ✔ 16-bit floating point numbers (float16), Requires NET 8 or higher, `Float16Array`, `Math.f16round()`
+- ✔ Array.fromAsync
- ✔ Import attributes
- ✔ Iterator helper methods
- ✔ JSON modules