From 7e2bcf88f8b033a19cc79e90a75139b8a25e7256 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 4 Jan 2026 22:09:00 +0200 Subject: [PATCH 1/5] Implement Array.fromAsync --- Jint.Tests.Test262/Test262Harness.settings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 073635a7b1..364816748b 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", From 205e998838420b72ef6f47f268e23d106caf48ab Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 4 Jan 2026 22:40:12 +0200 Subject: [PATCH 2/5] wip --- Jint/Native/Array/ArrayConstructor.cs | 409 +++++++++++++++++++++++++- 1 file changed, 408 insertions(+), 1 deletion(-) diff --git a/Jint/Native/Array/ArrayConstructor.cs b/Jint/Native/Array/ArrayConstructor.cs index 5306f25158..4e84d40328 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,410 @@ 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) + { + System.Console.WriteLine($"[DEBUG] FromAsyncIteratorLoop k={k}, iterator type={iterator.GetType().Name}"); + 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); + System.Console.WriteLine($"[DEBUG] onFulfilled k={capturedK}, done={done}, value={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 + System.Console.WriteLine($"[DEBUG] continueLoop called, will queue k={capturedK + 1}"); + _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, From 3a5f4c04037cb0ae8c72d5859c0fb767a1ffd4da Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 4 Jan 2026 23:10:56 +0200 Subject: [PATCH 3/5] cleanup --- Jint/Engine.cs | 1 - .../Native/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs | 3 +-- Jint/Runtime/Interpreter/JintFunctionDefinition.cs | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Jint/Engine.cs b/Jint/Engine.cs index 3d35897a67..722699588a 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/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs b/Jint/Native/AsyncGenerator/AsyncGeneratorFunctionConstructor.cs index c86ad4fd0c..3fc38298e1 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 768cda9a50..25f6e88f40 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; From 0cacb0de43d661f8ad0da9022001506a911fd235 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 5 Jan 2026 08:28:02 +0200 Subject: [PATCH 4/5] fix --- Jint/Native/Array/ArrayConstructor.cs | 3 --- .../Statements/JintForStatement.cs | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Jint/Native/Array/ArrayConstructor.cs b/Jint/Native/Array/ArrayConstructor.cs index 4e84d40328..17beb2e7b4 100644 --- a/Jint/Native/Array/ArrayConstructor.cs +++ b/Jint/Native/Array/ArrayConstructor.cs @@ -223,7 +223,6 @@ private void FromAsyncIteratorLoop( PromiseCapability promiseCapability, ulong k) { - System.Console.WriteLine($"[DEBUG] FromAsyncIteratorLoop k={k}, iterator type={iterator.GetType().Name}"); try { // Get next method and call it @@ -251,7 +250,6 @@ private void FromAsyncIteratorLoop( var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); var value = iterResultObj.Get(CommonProperties.Value); - System.Console.WriteLine($"[DEBUG] onFulfilled k={capturedK}, done={done}, value={value}"); if (done) { // Iteration complete @@ -263,7 +261,6 @@ private void FromAsyncIteratorLoop( ProcessAsyncIteratorValue(value, target, callable, thisArg, promiseCapability, capturedK, () => { // Queue next iteration to prevent stack overflow - System.Console.WriteLine($"[DEBUG] continueLoop called, will queue k={capturedK + 1}"); _engine.AddToEventLoop(() => FromAsyncIteratorLoop(iterator, target, callable, thisArg, promiseCapability, capturedK + 1)); }); return Undefined; diff --git a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs index a80a7fd502..429bba8074 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); } From 1066961fec769d09da611d168b61cab7bd86bffa Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 5 Jan 2026 08:36:13 +0200 Subject: [PATCH 5/5] tick the box --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 68821a50b0..ca5b162d5e 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