From eee7c34f824fba3bdff2d2603f253676650925f1 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 26 Mar 2026 10:41:07 +0200 Subject: [PATCH] Implement AsyncIterator Helpers (constructor, from, prototype methods) Add AsyncIterator as a global constructor with: - AsyncIterator.from() static method for wrapping async/sync iterables - Prototype helper methods: map, filter, take, drop, flatMap - Prototype reducing methods: reduce, toArray, forEach, some, every, find - Symbol.toStringTag returning "AsyncIterator" - WrapForValidAsyncIterator for non-instanceof wrapping Mirrors the existing sync Iterator Helpers architecture with promise-based chaining via PerformPromiseThen for all async operations. Co-Authored-By: Claude Opus 4.6 (1M context) --- Jint/Native/Global/GlobalObject.Properties.cs | 4 +- .../Iterator/AsyncIteratorConstructor.cs | 269 +++++++ Jint/Native/Iterator/AsyncIteratorHelper.cs | 762 ++++++++++++++++++ .../Iterator/AsyncIteratorHelperPrototype.cs | 58 ++ .../Native/Iterator/AsyncIteratorPrototype.cs | 540 ++++++++++++- Jint/Runtime/Intrinsics.cs | 12 + 6 files changed, 1621 insertions(+), 24 deletions(-) create mode 100644 Jint/Native/Iterator/AsyncIteratorConstructor.cs create mode 100644 Jint/Native/Iterator/AsyncIteratorHelper.cs create mode 100644 Jint/Native/Iterator/AsyncIteratorHelperPrototype.cs diff --git a/Jint/Native/Global/GlobalObject.Properties.cs b/Jint/Native/Global/GlobalObject.Properties.cs index cefdc75cea..4fd04646d3 100644 --- a/Jint/Native/Global/GlobalObject.Properties.cs +++ b/Jint/Native/Global/GlobalObject.Properties.cs @@ -11,6 +11,7 @@ public partial class GlobalObject private static readonly Key propertyArray = "Array"; private static readonly Key propertyArrayBuffer = "ArrayBuffer"; private static readonly Key propertyAsyncDisposableStack = "AsyncDisposableStack"; + private static readonly Key propertyAsyncIterator = "AsyncIterator"; private static readonly Key propertyAtomics = "Atomics"; private static readonly Key propertyBigInt = "BigInt"; private static readonly Key propertyBigInt64Array = "BigInt64Array"; @@ -87,11 +88,12 @@ protected override void Initialize() const PropertyFlag LengthFlags = PropertyFlag.Configurable; const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable; - var properties = new StringDictionarySlim(70); + var properties = new StringDictionarySlim(71); properties.AddDangerous(propertyAggregateError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.AggregateError, PropertyFlags)); properties.AddDangerous(propertyArray, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Array, PropertyFlags)); properties.AddDangerous(propertyArrayBuffer, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.ArrayBuffer, PropertyFlags)); properties.AddDangerous(propertyAsyncDisposableStack, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.AsyncDisposableStack, PropertyFlags)); + properties.AddDangerous(propertyAsyncIterator, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.AsyncIterator, PropertyFlags)); properties.AddDangerous(propertyAtomics, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Atomics, PropertyFlags)); properties.AddDangerous(propertyBigInt, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.BigInt, PropertyFlags)); properties.AddDangerous(propertyBigInt64Array, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.BigInt64Array, PropertyFlags)); diff --git a/Jint/Native/Iterator/AsyncIteratorConstructor.cs b/Jint/Native/Iterator/AsyncIteratorConstructor.cs new file mode 100644 index 0000000000..a0aa4e764c --- /dev/null +++ b/Jint/Native/Iterator/AsyncIteratorConstructor.cs @@ -0,0 +1,269 @@ +using Jint.Native.Function; +using Jint.Native.Object; +using Jint.Native.Promise; +using Jint.Native.Symbol; +using Jint.Runtime; +using Jint.Runtime.Descriptors; +using Jint.Runtime.Interop; + +namespace Jint.Native.Iterator; + +/// +/// https://tc39.es/ecma262/#sec-asynciterator-constructor +/// +internal sealed class AsyncIteratorConstructor : Constructor +{ + private static readonly JsString _functionName = new("AsyncIterator"); + + internal AsyncIteratorConstructor( + Engine engine, + Realm realm, + FunctionPrototype functionPrototype, + AsyncIteratorPrototype asyncIteratorPrototype) + : base(engine, realm, _functionName) + { + _prototype = functionPrototype; + PrototypeObject = asyncIteratorPrototype; + _length = new PropertyDescriptor(0, PropertyFlag.Configurable); + _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden); + } + + internal AsyncIteratorPrototype PrototypeObject { get; } + + protected override void Initialize() + { + const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable; + const PropertyFlag LengthFlags = PropertyFlag.Configurable; + var properties = new PropertyDictionary(1, checkExistingKeys: false) + { + ["from"] = new(new PropertyDescriptor(new ClrFunction(Engine, "from", From, 1, LengthFlags), PropertyFlags)), + }; + SetProperties(properties); + } + + public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) + { + if (newTarget.IsUndefined() || ReferenceEquals(this, newTarget)) + { + Throw.TypeError(_realm); + } + + return OrdinaryCreateFromConstructor( + newTarget, + static intrinsics => intrinsics.AsyncIterator.PrototypeObject, + static (Engine engine, Realm realm, object? _) => new JsObject(engine)); + } + + /// + /// https://tc39.es/ecma262/#sec-asynciterator.from + /// + private JsValue From(JsValue thisObject, JsValue[] arguments) + { + var o = arguments.At(0); + + // 1. If O is not an Object, throw a TypeError exception. + if (o is not ObjectInstance obj) + { + Throw.TypeError(_realm, "AsyncIterator.from requires an object"); + return Undefined; + } + + ObjectInstance iterator; + + // 2. Let asyncIteratorMethod be ? GetMethod(O, @@asyncIterator). + var asyncIteratorMethod = obj.GetMethod(GlobalSymbolRegistry.AsyncIterator); + + if (asyncIteratorMethod is not null) + { + // 3. If asyncIteratorMethod is not undefined, then + // a. Let asyncIterator be ? Call(asyncIteratorMethod, O). + var result = asyncIteratorMethod.Call(obj); + if (result is not ObjectInstance asyncIter) + { + Throw.TypeError(_realm, "Async iterator result is not an object"); + return Undefined; + } + iterator = asyncIter; + } + else + { + // 4. Else, + // a. Let syncIteratorMethod be ? GetMethod(O, @@iterator). + var syncIteratorMethod = obj.GetMethod(GlobalSymbolRegistry.Iterator); + + if (syncIteratorMethod is not null) + { + // b. If syncIteratorMethod is not undefined, then + // i. Let syncIterator be ? Call(syncIteratorMethod, O). + var syncResult = syncIteratorMethod.Call(obj); + if (syncResult is not ObjectInstance syncIter) + { + Throw.TypeError(_realm, "Iterator result is not an object"); + return Undefined; + } + + // ii. Return ! CreateAsyncFromSyncIterator(syncIterator). + var syncIteratorInstance = new IteratorInstance.ObjectIterator(syncIter); + return new AsyncFromSyncIterator(_engine, syncIteratorInstance); + } + + // c. Let iteratorRecord be GetIteratorDirect(O) - use object directly. + iterator = obj; + } + + // 5. Let hasInstance be ? OrdinaryHasInstance(%AsyncIterator%, iterator). + var hasInstance = _engine.Intrinsics.AsyncIterator.OrdinaryHasInstance(iterator); + + // 6. If hasInstance is true, return iterator. + if (TypeConverter.ToBoolean(hasInstance)) + { + return iterator; + } + + // 7. Let wrapper be WrapForValidAsyncIterator + var iteratorRecord = new IteratorInstance.ObjectIterator(iterator); + var wrapper = new WrapForValidAsyncIterator(_engine, iteratorRecord); + return wrapper; + } +} + +/// +/// Wrapper instance for AsyncIterator.from when the input is not already an AsyncIterator instance. +/// +internal sealed class WrapForValidAsyncIterator : ObjectInstance +{ + internal readonly IteratorInstance.ObjectIterator Iterated; + + public WrapForValidAsyncIterator(Engine engine, IteratorInstance.ObjectIterator iterated) : base(engine) + { + Iterated = iterated; + _prototype = engine.Realm.Intrinsics.WrapForValidAsyncIteratorPrototype; + } +} + +/// +/// https://tc39.es/ecma262/#sec-%wrapforvalidasynciteratorprototype%-object +/// +internal sealed class WrapForValidAsyncIteratorPrototype : Prototype +{ + internal WrapForValidAsyncIteratorPrototype( + Engine engine, + Realm realm, + AsyncIteratorPrototype asyncIteratorPrototype) : base(engine, realm) + { + _prototype = asyncIteratorPrototype; + } + + protected override void Initialize() + { + var properties = new PropertyDictionary(2, checkExistingKeys: false) + { + [KnownKeys.Next] = new PropertyDescriptor(new ClrFunction(_engine, "next", Next, 0, PropertyFlag.Configurable), PropertyFlag.Writable | PropertyFlag.Configurable), + [KnownKeys.Return] = new PropertyDescriptor(new ClrFunction(_engine, "return", Return, 0, PropertyFlag.Configurable), PropertyFlag.Writable | PropertyFlag.Configurable), + }; + SetProperties(properties); + } + + /// + /// %WrapForValidAsyncIteratorPrototype%.next() + /// Delegates to the underlying iterator's next() and wraps in a promise. + /// + private JsValue Next(JsValue thisObject, JsValue[] arguments) + { + if (thisObject is not WrapForValidAsyncIterator wrapper) + { + Throw.TypeError(_realm, "Method WrapForValidAsyncIterator.prototype.next called on incompatible receiver"); + return Undefined; + } + + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); + + try + { + // Call the underlying iterator's next method + var target = wrapper.Iterated.Instance; + var nextMethod = target.Get(CommonProperties.Next); + if (nextMethod is not ICallable callable) + { + Throw.TypeError(_realm, "Iterator does not have a next method"); + return Undefined; + } + + var result = callable.Call(target, Arguments.Empty); + + // Normalize the result to a promise + var resultPromise = (JsPromise) _realm.Intrinsics.Promise.PromiseResolve(result); + + // Chain to extract the iterator result + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + throw new JavaScriptException(_realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + } + return iterResultObj; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, resultPromise, onFulfilled, Undefined, promiseCapability); + } + catch (JavaScriptException ex) + { + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + } + + return promiseCapability.PromiseInstance; + } + + /// + /// %WrapForValidAsyncIteratorPrototype%.return() + /// + private JsValue Return(JsValue thisObject, JsValue[] arguments) + { + if (thisObject is not WrapForValidAsyncIterator wrapper) + { + Throw.TypeError(_realm, "Method WrapForValidAsyncIterator.prototype.return called on incompatible receiver"); + return Undefined; + } + + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); + + try + { + var iterator = wrapper.Iterated.Instance; + var returnMethod = iterator.GetMethod(CommonProperties.Return); + + if (returnMethod is null) + { + // Return a resolved promise with {value: undefined, done: true} + var doneResult = IteratorResult.CreateValueIteratorPosition(_engine, Undefined, JsBoolean.True); + promiseCapability.Resolve.Call(Undefined, new JsValue[] { doneResult }); + } + else + { + var result = returnMethod.Call(iterator, Arguments.Empty); + + // Normalize the result to a promise + var resultPromise = (JsPromise) _realm.Intrinsics.Promise.PromiseResolve(result); + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + throw new JavaScriptException(_realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + } + return iterResultObj; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, resultPromise, onFulfilled, Undefined, promiseCapability); + } + } + catch (JavaScriptException ex) + { + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + } + + return promiseCapability.PromiseInstance; + } +} diff --git a/Jint/Native/Iterator/AsyncIteratorHelper.cs b/Jint/Native/Iterator/AsyncIteratorHelper.cs new file mode 100644 index 0000000000..1609c6a789 --- /dev/null +++ b/Jint/Native/Iterator/AsyncIteratorHelper.cs @@ -0,0 +1,762 @@ +using Jint.Native.Generator; +using Jint.Native.Object; +using Jint.Native.Promise; +using Jint.Runtime; +using Jint.Runtime.Descriptors; +using Jint.Runtime.Interop; + +namespace Jint.Native.Iterator; + +/// +/// Base class for async iterator helper instances (map, filter, take, drop, flatMap). +/// https://tc39.es/ecma262/#sec-asynciteratorhelper-objects +/// +internal abstract class AsyncIteratorHelper : ObjectInstance +{ + protected readonly IteratorInstance.ObjectIterator Iterated; + protected GeneratorState State; + protected int Counter; + protected bool Exhausted; + + protected AsyncIteratorHelper(Engine engine, IteratorInstance.ObjectIterator iterated) : base(engine) + { + Iterated = iterated; + State = GeneratorState.SuspendedStart; + Counter = 0; + Exhausted = false; + _prototype = engine.Realm.Intrinsics.AsyncIteratorHelperPrototype; + } + + /// + /// Called by AsyncIteratorHelperPrototype.next() to get a promise of the next value. + /// + public JsValue Next() + { + if (State == GeneratorState.Executing) + { + Throw.TypeError(_engine.Realm, "Generator is already executing"); + return Undefined; + } + + if (State == GeneratorState.Completed) + { + return CreateResolvedIteratorResultPromise(Undefined, done: true); + } + + State = GeneratorState.Executing; + + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + try + { + ExecuteStep(promiseCapability); + } + catch (JavaScriptException ex) + { + State = GeneratorState.Completed; + if (!Exhausted) + { + Exhausted = true; + try { Iterated.Close(CompletionType.Throw); } catch { /* ignore */ } + } + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + } + + return promiseCapability.PromiseInstance; + } + + /// + /// Called by AsyncIteratorHelperPrototype.return() to close the helper. + /// + public virtual JsValue Return() + { + State = GeneratorState.Completed; + + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + if (!Exhausted) + { + Exhausted = true; + try + { + var iterator = Iterated.Instance; + var returnMethod = iterator.GetMethod(CommonProperties.Return); + if (returnMethod is not null) + { + var returnResult = returnMethod.Call(iterator, Arguments.Empty); + var returnPromise = (JsPromise) _engine.Realm.Intrinsics.Promise.PromiseResolve(returnResult); + + var onFulfilled = new ClrFunction(_engine, "", (_, _) => + { + promiseCapability.Resolve.Call(Undefined, new JsValue[] { CreateIteratorResult(Undefined, done: true) }); + return Undefined; + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + promiseCapability.Reject.Call(Undefined, new[] { args.At(0) }); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, returnPromise, onFulfilled, onRejected, null!); + return promiseCapability.PromiseInstance; + } + } + catch (JavaScriptException ex) + { + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + return promiseCapability.PromiseInstance; + } + } + + var doneResult = CreateIteratorResult(Undefined, done: true); + promiseCapability.Resolve.Call(Undefined, new JsValue[] { doneResult }); + return promiseCapability.PromiseInstance; + } + + protected abstract void ExecuteStep(PromiseCapability promiseCapability); + + /// + /// Calls the underlying iterator's next() and normalizes the result as a promise. + /// + protected JsPromise CallIteratorNext() + { + var target = Iterated.Instance; + var nextMethod = target.Get(CommonProperties.Next); + if (nextMethod is not ICallable callable) + { + Throw.TypeError(_engine.Realm, "Iterator does not have a next method"); + return null!; + } + + var result = callable.Call(target, Arguments.Empty); + return (JsPromise) _engine.Realm.Intrinsics.Promise.PromiseResolve(result); + } + + protected void CloseIterator(CompletionType completionType) + { + Iterated.Close(completionType); + } + + protected ObjectInstance CreateIteratorResult(JsValue value, bool done) + { + return IteratorResult.CreateValueIteratorPosition(_engine, value, JsBoolean.Create(done)); + } + + protected JsValue CreateResolvedIteratorResultPromise(JsValue value, bool done) + { + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + var result = CreateIteratorResult(value, done); + promiseCapability.Resolve.Call(Undefined, new JsValue[] { result }); + return promiseCapability.PromiseInstance; + } + + /// + /// Helper to resolve promiseCapability with an iterator result {value, done: false}, + /// setting state to SuspendedYield. + /// + protected void ResolveYield(PromiseCapability promiseCapability, JsValue value) + { + State = GeneratorState.SuspendedYield; + promiseCapability.Resolve.Call(Undefined, new JsValue[] { CreateIteratorResult(value, done: false) }); + } + + /// + /// Helper to resolve promiseCapability with {value: undefined, done: true}, + /// setting state to Completed. + /// + protected void ResolveDone(PromiseCapability promiseCapability) + { + State = GeneratorState.Completed; + Exhausted = true; + promiseCapability.Resolve.Call(Undefined, new JsValue[] { CreateIteratorResult(Undefined, done: true) }); + } + + /// + /// Helper to reject promiseCapability with an error. + /// + protected void RejectWithError(PromiseCapability promiseCapability, JsValue error) + { + State = GeneratorState.Completed; + Exhausted = true; + try { Iterated.Close(CompletionType.Throw); } catch { /* ignore */ } + promiseCapability.Reject.Call(Undefined, new[] { error }); + } +} + +/// +/// Async iterator helper for map(mapper) - transforms each element. +/// +internal sealed class AsyncMapIterator : AsyncIteratorHelper +{ + private readonly ICallable _mapper; + + public AsyncMapIterator(Engine engine, IteratorInstance.ObjectIterator iterated, ICallable mapper) : base(engine, iterated) + { + _mapper = mapper; + } + + protected override void ExecuteStep(PromiseCapability promiseCapability) + { + var nextPromise = CallIteratorNext(); + var self = this; + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + self.RejectWithError(promiseCapability, self._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + self.ResolveDone(promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + var counter = self.Counter; + self.Counter++; + + JsValue mapped; + try + { + mapped = self._mapper.Call(Undefined, new[] { value, (JsValue) counter }); + } + catch (JavaScriptException ex) + { + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + + // The mapped value could be a promise, so resolve it + var mappedPromise = (JsPromise) self._engine.Realm.Intrinsics.Promise.PromiseResolve(mapped); + + var onMappedFulfilled = new ClrFunction(self._engine, "", (_, innerArgs) => + { + self.ResolveYield(promiseCapability, innerArgs.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + var onMappedRejected = new ClrFunction(self._engine, "", (_, innerArgs) => + { + self.RejectWithError(promiseCapability, innerArgs.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(self._engine, mappedPromise, onMappedFulfilled, onMappedRejected, null!); + return Undefined; + } + catch (JavaScriptException ex) + { + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + self.RejectWithError(promiseCapability, args.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, nextPromise, onFulfilled, onRejected, null!); + } +} + +/// +/// Async iterator helper for filter(predicate) - returns elements matching the predicate. +/// +internal sealed class AsyncFilterIterator : AsyncIteratorHelper +{ + private readonly ICallable _predicate; + + public AsyncFilterIterator(Engine engine, IteratorInstance.ObjectIterator iterated, ICallable predicate) : base(engine, iterated) + { + _predicate = predicate; + } + + protected override void ExecuteStep(PromiseCapability promiseCapability) + { + FilterNextStep(promiseCapability); + } + + private void FilterNextStep(PromiseCapability promiseCapability) + { + var nextPromise = CallIteratorNext(); + var self = this; + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + self.RejectWithError(promiseCapability, self._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + self.ResolveDone(promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + var counter = self.Counter; + self.Counter++; + + JsValue selected; + try + { + selected = self._predicate.Call(Undefined, new[] { value, (JsValue) counter }); + } + catch (JavaScriptException ex) + { + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + + var selectedPromise = (JsPromise) self._engine.Realm.Intrinsics.Promise.PromiseResolve(selected); + + var onSelectedFulfilled = new ClrFunction(self._engine, "", (_, innerArgs) => + { + var resolvedSelected = innerArgs.At(0); + if (TypeConverter.ToBoolean(resolvedSelected)) + { + self.ResolveYield(promiseCapability, value); + } + else + { + // Value didn't pass filter, try the next one + self.FilterNextStep(promiseCapability); + } + return Undefined; + }, 1, PropertyFlag.Configurable); + + var onSelectedRejected = new ClrFunction(self._engine, "", (_, innerArgs) => + { + self.RejectWithError(promiseCapability, innerArgs.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(self._engine, selectedPromise, onSelectedFulfilled, onSelectedRejected, null!); + return Undefined; + } + catch (JavaScriptException ex) + { + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + self.RejectWithError(promiseCapability, args.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, nextPromise, onFulfilled, onRejected, null!); + } +} + +/// +/// Async iterator helper for take(limit) - returns the first N elements. +/// +internal sealed class AsyncTakeIterator : AsyncIteratorHelper +{ + private readonly long _limit; + private long _taken; + + public AsyncTakeIterator(Engine engine, IteratorInstance.ObjectIterator iterated, long limit) : base(engine, iterated) + { + _limit = limit; + _taken = 0; + } + + protected override void ExecuteStep(PromiseCapability promiseCapability) + { + if (_taken >= _limit) + { + State = GeneratorState.Completed; + Exhausted = true; + try { Iterated.Close(CompletionType.Normal); } catch { /* ignore */ } + promiseCapability.Resolve.Call(Undefined, new JsValue[] { CreateIteratorResult(Undefined, done: true) }); + return; + } + + var nextPromise = CallIteratorNext(); + var self = this; + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + self.RejectWithError(promiseCapability, self._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + self.ResolveDone(promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + self._taken++; + self.ResolveYield(promiseCapability, value); + return Undefined; + } + catch (JavaScriptException ex) + { + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + self.RejectWithError(promiseCapability, args.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, nextPromise, onFulfilled, onRejected, null!); + } +} + +/// +/// Async iterator helper for drop(limit) - skips the first N elements. +/// +internal sealed class AsyncDropIterator : AsyncIteratorHelper +{ + private long _remaining; + private bool _dropping; + + public AsyncDropIterator(Engine engine, IteratorInstance.ObjectIterator iterated, long limit) : base(engine, iterated) + { + _remaining = limit; + _dropping = limit > 0; + } + + protected override void ExecuteStep(PromiseCapability promiseCapability) + { + DropStep(promiseCapability); + } + + private void DropStep(PromiseCapability promiseCapability) + { + var nextPromise = CallIteratorNext(); + var self = this; + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + self.RejectWithError(promiseCapability, self._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + self.ResolveDone(promiseCapability); + return Undefined; + } + + if (self._dropping) + { + self._remaining--; + if (self._remaining <= 0) + { + self._dropping = false; + } + // Skip this value, continue dropping + self.DropStep(promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + self.ResolveYield(promiseCapability, value); + return Undefined; + } + catch (JavaScriptException ex) + { + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + self.RejectWithError(promiseCapability, args.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, nextPromise, onFulfilled, onRejected, null!); + } +} + +/// +/// Async iterator helper for flatMap(mapper) - maps and flattens one level. +/// +internal sealed class AsyncFlatMapIterator : AsyncIteratorHelper +{ + private readonly ICallable _mapper; + private IteratorInstance.ObjectIterator? _innerIterator; + + public AsyncFlatMapIterator(Engine engine, IteratorInstance.ObjectIterator iterated, ICallable mapper) : base(engine, iterated) + { + _mapper = mapper; + } + + public override JsValue Return() + { + State = GeneratorState.Completed; + + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + try + { + if (_innerIterator is not null) + { + var inner = _innerIterator; + _innerIterator = null; + inner.Close(CompletionType.Return); + } + + if (!Exhausted) + { + Exhausted = true; + var iterator = Iterated.Instance; + var returnMethod = iterator.GetMethod(CommonProperties.Return); + if (returnMethod is not null) + { + var returnResult = returnMethod.Call(iterator, Arguments.Empty); + var returnPromise = (JsPromise) _engine.Realm.Intrinsics.Promise.PromiseResolve(returnResult); + + var onFulfilled = new ClrFunction(_engine, "", (_, _) => + { + promiseCapability.Resolve.Call(Undefined, new JsValue[] { CreateIteratorResult(Undefined, done: true) }); + return Undefined; + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + promiseCapability.Reject.Call(Undefined, new[] { args.At(0) }); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, returnPromise, onFulfilled, onRejected, null!); + return promiseCapability.PromiseInstance; + } + } + } + catch (JavaScriptException ex) + { + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + return promiseCapability.PromiseInstance; + } + + promiseCapability.Resolve.Call(Undefined, new JsValue[] { CreateIteratorResult(Undefined, done: true) }); + return promiseCapability.PromiseInstance; + } + + protected override void ExecuteStep(PromiseCapability promiseCapability) + { + FlatMapStep(promiseCapability); + } + + private void FlatMapStep(PromiseCapability promiseCapability) + { + // If we have an inner iterator, consume from it first + if (_innerIterator is not null) + { + var innerTarget = _innerIterator.Instance; + var innerNextMethod = innerTarget.Get(CommonProperties.Next); + if (innerNextMethod is not ICallable innerCallable) + { + _innerIterator = null; + FlatMapStep(promiseCapability); + return; + } + + var innerResult = innerCallable.Call(innerTarget, Arguments.Empty); + var innerPromise = (JsPromise) _engine.Realm.Intrinsics.Promise.PromiseResolve(innerResult); + var self = this; + + var onInnerFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + self._innerIterator = null; + self.FlatMapStep(promiseCapability); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + self._innerIterator = null; + self.FlatMapStep(promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + self.ResolveYield(promiseCapability, value); + return Undefined; + } + catch (JavaScriptException ex) + { + self._innerIterator = null; + self.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onInnerRejected = new ClrFunction(_engine, "", (_, args) => + { + self._innerIterator = null; + self.RejectWithError(promiseCapability, args.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, innerPromise, onInnerFulfilled, onInnerRejected, null!); + return; + } + + // Get next from outer iterator + var nextPromise = CallIteratorNext(); + var outerSelf = this; + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + outerSelf.RejectWithError(promiseCapability, outerSelf._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + outerSelf.ResolveDone(promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + var counter = outerSelf.Counter; + outerSelf.Counter++; + + JsValue mapped; + try + { + mapped = outerSelf._mapper.Call(Undefined, new[] { value, (JsValue) counter }); + } + catch (JavaScriptException ex) + { + outerSelf.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + + var mappedPromise = (JsPromise) outerSelf._engine.Realm.Intrinsics.Promise.PromiseResolve(mapped); + + var onMappedFulfilled = new ClrFunction(outerSelf._engine, "", (_, innerArgs) => + { + try + { + var resolvedMapped = innerArgs.At(0); + + if (resolvedMapped is not ObjectInstance mappedObj) + { + outerSelf.RejectWithError(promiseCapability, outerSelf._engine.Realm.Intrinsics.TypeError.Construct("flatMap mapper must return an iterable or iterator")); + return Undefined; + } + + // Try @@asyncIterator first, then @@iterator, then use directly + ObjectInstance innerIteratorObj; + var asyncMethod = mappedObj.GetMethod(Symbol.GlobalSymbolRegistry.AsyncIterator); + if (asyncMethod is not null) + { + var asyncResult = asyncMethod.Call(mappedObj); + if (asyncResult is not ObjectInstance asyncObj) + { + outerSelf.RejectWithError(promiseCapability, outerSelf._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + innerIteratorObj = asyncObj; + } + else + { + var syncMethod = mappedObj.GetMethod(Symbol.GlobalSymbolRegistry.Iterator); + if (syncMethod is not null) + { + var syncResult = syncMethod.Call(mappedObj); + if (syncResult is not ObjectInstance syncObj) + { + outerSelf.RejectWithError(promiseCapability, outerSelf._engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object")); + return Undefined; + } + innerIteratorObj = syncObj; + } + else + { + innerIteratorObj = mappedObj; + } + } + + outerSelf._innerIterator = new IteratorInstance.ObjectIterator(innerIteratorObj); + outerSelf.FlatMapStep(promiseCapability); + return Undefined; + } + catch (JavaScriptException ex) + { + outerSelf.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onMappedRejected = new ClrFunction(outerSelf._engine, "", (_, innerArgs) => + { + outerSelf.RejectWithError(promiseCapability, innerArgs.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(outerSelf._engine, mappedPromise, onMappedFulfilled, onMappedRejected, null!); + return Undefined; + } + catch (JavaScriptException ex) + { + outerSelf.RejectWithError(promiseCapability, ex.Error); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + outerSelf.RejectWithError(promiseCapability, args.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, nextPromise, onFulfilled, onRejected, null!); + } +} diff --git a/Jint/Native/Iterator/AsyncIteratorHelperPrototype.cs b/Jint/Native/Iterator/AsyncIteratorHelperPrototype.cs new file mode 100644 index 0000000000..5e137a85fa --- /dev/null +++ b/Jint/Native/Iterator/AsyncIteratorHelperPrototype.cs @@ -0,0 +1,58 @@ +using Jint.Runtime; +using Jint.Runtime.Descriptors; +using Jint.Runtime.Interop; + +namespace Jint.Native.Iterator; + +/// +/// https://tc39.es/ecma262/#sec-%asynciteratorhelperprototype%-object +/// The %AsyncIteratorHelperPrototype% object is the prototype of async iterator helper objects. +/// +internal sealed class AsyncIteratorHelperPrototype : Prototype +{ + internal AsyncIteratorHelperPrototype( + Engine engine, + Realm realm, + AsyncIteratorPrototype asyncIteratorPrototype) : base(engine, realm) + { + _prototype = asyncIteratorPrototype; + } + + protected override void Initialize() + { + var properties = new PropertyDictionary(2, checkExistingKeys: false) + { + [KnownKeys.Next] = new PropertyDescriptor(new ClrFunction(_engine, "next", Next, 0, PropertyFlag.Configurable), PropertyFlag.Writable | PropertyFlag.Configurable), + [KnownKeys.Return] = new PropertyDescriptor(new ClrFunction(_engine, "return", Return, 0, PropertyFlag.Configurable), PropertyFlag.Writable | PropertyFlag.Configurable), + }; + SetProperties(properties); + } + + /// + /// %AsyncIteratorHelperPrototype%.next() + /// + private JsValue Next(JsValue thisObject, JsValue[] arguments) + { + if (thisObject is AsyncIteratorHelper helper) + { + return helper.Next(); + } + + Throw.TypeError(_realm, "Method AsyncIterator Helper.prototype.next called on incompatible receiver"); + return Undefined; + } + + /// + /// %AsyncIteratorHelperPrototype%.return() + /// + private JsValue Return(JsValue thisObject, JsValue[] arguments) + { + if (thisObject is AsyncIteratorHelper helper) + { + return helper.Return(); + } + + Throw.TypeError(_realm, "Method AsyncIterator Helper.prototype.return called on incompatible receiver"); + return Undefined; + } +} diff --git a/Jint/Native/Iterator/AsyncIteratorPrototype.cs b/Jint/Native/Iterator/AsyncIteratorPrototype.cs index 648b41346e..3de49d1499 100644 --- a/Jint/Native/Iterator/AsyncIteratorPrototype.cs +++ b/Jint/Native/Iterator/AsyncIteratorPrototype.cs @@ -23,18 +23,288 @@ internal AsyncIteratorPrototype( protected override void Initialize() { const PropertyFlag Flags = PropertyFlag.Writable | PropertyFlag.Configurable; - var symbols = new SymbolDictionary(2) + + var properties = new PropertyDictionary(12, checkExistingKeys: false) + { + [KnownKeys.Constructor] = new GetSetPropertyDescriptor( + get: new ClrFunction(_engine, "get constructor", (_, _) => _engine.Intrinsics.AsyncIterator, 0, PropertyFlag.Configurable), + set: new ClrFunction(_engine, "set constructor", (thisObject, arguments) => + { + SetterThatIgnoresPrototypeProperties(thisObject, _engine.Intrinsics.AsyncIterator.PrototypeObject, CommonProperties.Constructor, arguments.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable), + PropertyFlag.Configurable), + ["map"] = new(new ClrFunction(_engine, "map", Map, 1, PropertyFlag.Configurable), Flags), + ["filter"] = new(new ClrFunction(_engine, "filter", Filter, 1, PropertyFlag.Configurable), Flags), + ["take"] = new(new ClrFunction(_engine, "take", Take, 1, PropertyFlag.Configurable), Flags), + ["drop"] = new(new ClrFunction(_engine, "drop", Drop, 1, PropertyFlag.Configurable), Flags), + ["flatMap"] = new(new ClrFunction(_engine, "flatMap", FlatMap, 1, PropertyFlag.Configurable), Flags), + ["reduce"] = new(new ClrFunction(_engine, "reduce", Reduce, 1, PropertyFlag.Configurable), Flags), + ["toArray"] = new(new ClrFunction(_engine, "toArray", ToArray, 0, PropertyFlag.Configurable), Flags), + ["forEach"] = new(new ClrFunction(_engine, "forEach", ForEach, 1, PropertyFlag.Configurable), Flags), + ["some"] = new(new ClrFunction(_engine, "some", Some, 1, PropertyFlag.Configurable), Flags), + ["every"] = new(new ClrFunction(_engine, "every", Every, 1, PropertyFlag.Configurable), Flags), + ["find"] = new(new ClrFunction(_engine, "find", Find, 1, PropertyFlag.Configurable), Flags), + }; + SetProperties(properties); + + var symbols = new SymbolDictionary(3) { [GlobalSymbolRegistry.AsyncIterator] = new(new ClrFunction(Engine, "[Symbol.asyncIterator]", AsyncIterator, 0, PropertyFlag.Configurable), Flags), - [GlobalSymbolRegistry.AsyncDispose] = new(new ClrFunction(Engine, "[Symbol.asyncDispose]", AsyncDispose, 0, PropertyFlag.Configurable), Flags) + [GlobalSymbolRegistry.AsyncDispose] = new(new ClrFunction(Engine, "[Symbol.asyncDispose]", AsyncDispose, 0, PropertyFlag.Configurable), Flags), + [GlobalSymbolRegistry.ToStringTag] = new GetSetPropertyDescriptor( + get: new ClrFunction(_engine, "get [Symbol.toStringTag]", (_, _) => "AsyncIterator", 0, PropertyFlag.Configurable), + set: new ClrFunction(_engine, "set [Symbol.toStringTag]", (thisObject, arguments) => + { + SetterThatIgnoresPrototypeProperties(thisObject, _engine.Intrinsics.AsyncIterator.PrototypeObject, GlobalSymbolRegistry.ToStringTag, arguments.At(0)); + return Undefined; + }, 0, PropertyFlag.Configurable), + PropertyFlag.Configurable) }; SetSymbols(symbols); } + /// + /// https://tc39.es/ecma262/#sec-SetterThatIgnoresPrototypeProperties + /// + private void SetterThatIgnoresPrototypeProperties(JsValue thisValue, ObjectInstance home, JsValue p, JsValue v) + { + if (thisValue is not ObjectInstance objectInstance) + { + Throw.TypeError(_realm); + return; + } + + if (SameValue(thisValue, home)) + { + Throw.TypeError(_realm); + return; + } + + var desc = objectInstance.GetOwnProperty(p); + if (desc == PropertyDescriptor.Undefined) + { + objectInstance.CreateDataPropertyOrThrow(p, v); + } + else + { + objectInstance.Set(p, v, throwOnError: true); + } + } + + private static IteratorInstance.ObjectIterator GetIteratorDirect(ObjectInstance objectInstance) => new(objectInstance); + + /// + /// Close an iterator by calling its return() method directly. + /// + private static void IteratorClose(ObjectInstance obj, CompletionType completionType) + { + var returnMethod = obj.GetMethod(CommonProperties.Return); + if (returnMethod is null) + { + return; + } + + try + { + returnMethod.Call(obj, Arguments.Empty); + } + catch when (completionType == CompletionType.Throw) + { + // Ignore errors from return when completion is throw + } + } + + /// + /// Validates thisObject is an ObjectInstance and extracts a callable argument. + /// Closes the iterator on validation failure. + /// + private ObjectInstance ValidateThisAndGetCallable(JsValue thisObject, JsValue[] arguments, string methodName, out ICallable callable) + { + if (thisObject is not ObjectInstance o) + { + Throw.TypeError(_realm, $"AsyncIterator.prototype.{methodName} called on non-object"); + callable = null!; + return null!; + } + + try + { + callable = GetCallable(arguments.At(0)); + } + catch + { + IteratorClose(o, CompletionType.Throw); + throw; + } + + return o; + } + + /// + /// Validates thisObject and extracts a numeric limit argument. + /// Closes the iterator on validation failure. + /// + private ObjectInstance ValidateThisAndGetLimit(JsValue thisObject, JsValue[] arguments, string methodName, out long limit) + { + if (thisObject is not ObjectInstance o) + { + Throw.TypeError(_realm, $"AsyncIterator.prototype.{methodName} called on non-object"); + limit = 0; + return null!; + } + + double numLimit; + try + { + numLimit = TypeConverter.ToNumber(arguments.At(0)); + } + catch + { + IteratorClose(o, CompletionType.Throw); + throw; + } + + if (double.IsNaN(numLimit)) + { + IteratorClose(o, CompletionType.Throw); + Throw.RangeError(_realm, "Invalid limit"); + limit = 0; + return null!; + } + + var integerLimit = TypeConverter.ToIntegerOrInfinity(numLimit); + + if (integerLimit < 0) + { + IteratorClose(o, CompletionType.Throw); + Throw.RangeError(_realm, "Invalid limit"); + limit = 0; + return null!; + } + + limit = double.IsPositiveInfinity(integerLimit) ? long.MaxValue : (long) integerLimit; + return o; + } + + // ---- Shared async iteration infrastructure ---- + + /// + /// Calls the underlying iterator's next() and normalizes the result as a promise. + /// Returns null and rejects the capability on failure. + /// + private JsPromise? CallIteratorNext(IteratorInstance.ObjectIterator iterated, PromiseCapability promiseCapability) + { + try + { + var target = iterated.Instance; + var nextMethod = target.Get(CommonProperties.Next); + if (nextMethod is not ICallable callable) + { + Throw.TypeError(_realm, "Iterator does not have a next method"); + return null; + } + + var result = callable.Call(target, Arguments.Empty); + return (JsPromise) _engine.Realm.Intrinsics.Promise.PromiseResolve(result); + } + catch (JavaScriptException ex) + { + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + return null; + } + } + + /// + /// Shared async iteration loop. Calls next() on the iterator, extracts done/value, + /// and invokes the callback for each value. The callback handles method-specific logic + /// and calls continueLoop() to iterate further, or resolves/rejects the capability to finish. + /// + private void AsyncIterateLoop( + IteratorInstance.ObjectIterator iterated, + PromiseCapability promiseCapability, + Action onDone, + Action onValue, + int counter = 0) + { + var nextPromise = CallIteratorNext(iterated, promiseCapability); + if (nextPromise is null) + { + return; // Already rejected + } + + var capturedCounter = counter; + + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + try + { + var iterResult = args.At(0); + if (iterResult is not ObjectInstance iterResultObj) + { + promiseCapability.Reject.Call(Undefined, new JsValue[] { _engine.Realm.Intrinsics.TypeError.Construct("Iterator result is not an object") }); + return Undefined; + } + + var done = TypeConverter.ToBoolean(iterResultObj.Get(CommonProperties.Done)); + if (done) + { + onDone(Undefined, promiseCapability); + return Undefined; + } + + var value = iterResultObj.Get(CommonProperties.Value); + onValue(value, capturedCounter, promiseCapability); + return Undefined; + } + catch (JavaScriptException ex) + { + promiseCapability.Reject.Call(Undefined, new[] { ex.Error }); + return Undefined; + } + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, args) => + { + promiseCapability.Reject.Call(Undefined, new[] { args.At(0) }); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, nextPromise, onFulfilled, onRejected, null!); + } + + /// + /// Chains on a promise result from a callback (predicate/reducer/procedure), then invokes + /// the handler with the resolved value. Rejects and closes the iterator on failure. + /// + private void ChainCallbackResult( + IteratorInstance.ObjectIterator iterated, + JsValue callbackResult, + PromiseCapability promiseCapability, + Action onResolved) + { + var resultPromise = (JsPromise) _engine.Realm.Intrinsics.Promise.PromiseResolve(callbackResult); + + var onFulfilled = new ClrFunction(_engine, "", (_, innerArgs) => + { + onResolved(innerArgs.At(0)); + return Undefined; + }, 1, PropertyFlag.Configurable); + + var onRejected = new ClrFunction(_engine, "", (_, innerArgs) => + { + try { iterated.Close(CompletionType.Throw); } catch { /* ignore */ } + promiseCapability.Reject.Call(Undefined, new[] { innerArgs.At(0) }); + return Undefined; + }, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, resultPromise, onFulfilled, onRejected, null!); + } + + // ---- Symbol methods ---- + /// /// https://tc39.es/ecma262/#sec-asynciteratorprototype-asynciterator - /// %AsyncIteratorPrototype% [ @@asyncIterator ] ( ) - /// 1. Return the this value. /// private static JsValue AsyncIterator(JsValue thisObject, JsCallArguments arguments) { @@ -43,17 +313,12 @@ private static JsValue AsyncIterator(JsValue thisObject, JsCallArguments argumen /// /// https://tc39.es/ecma262/#sec-%asynciteratorprototype%-@@asyncDispose - /// %AsyncIteratorPrototype% [ @@asyncDispose ] ( ) /// private JsValue AsyncDispose(JsValue thisObject, JsCallArguments arguments) { - // 1. Let O be the this value. var o = thisObject; - - // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); - // 3. Let return be GetMethod(O, "return"). ICallable? returnMethod; try { @@ -61,34 +326,27 @@ private JsValue AsyncDispose(JsValue thisObject, JsCallArguments arguments) } catch (JavaScriptException ex) { - // 4. IfAbruptRejectPromise(return, promiseCapability). promiseCapability.Reject.Call(Undefined, ex.Error); return promiseCapability.PromiseInstance; } - // 5. If return is undefined, then if (returnMethod is null) { - // a. Perform ! Call(promiseCapability.[[Resolve]], undefined, « undefined »). promiseCapability.Resolve.Call(Undefined, Undefined); } else { - // 6. Else, JsValue result; try { - // a. Let result be Call(return, O, « undefined »). result = returnMethod.Call(o, Undefined); } catch (JavaScriptException ex) { - // b. IfAbruptRejectPromise(result, promiseCapability). promiseCapability.Reject.Call(Undefined, ex.Error); return promiseCapability.PromiseInstance; } - // c. Let resultWrapper be Completion(PromiseResolve(%Promise%, result)). JsPromise resultWrapper; try { @@ -96,21 +354,257 @@ private JsValue AsyncDispose(JsValue thisObject, JsCallArguments arguments) } catch (JavaScriptException ex) { - // d. IfAbruptRejectPromise(resultWrapper, promiseCapability). promiseCapability.Reject.Call(Undefined, ex.Error); return promiseCapability.PromiseInstance; } - // e. Let unwrap be a new Abstract Closure that performs the following steps when called: - // i. Return undefined. - // f. Let onFulfilled be CreateBuiltinFunction(unwrap, 1, "", « »). var onFulfilled = new ClrFunction(_engine, "", (_, _) => Undefined, 1, PropertyFlag.Configurable); - - // g. Perform PerformPromiseThen(resultWrapper, onFulfilled, undefined, promiseCapability). PromiseOperations.PerformPromiseThen(_engine, resultWrapper, onFulfilled, null!, promiseCapability); } - // 7. Return promiseCapability.[[Promise]]. return promiseCapability.PromiseInstance; } + + // ---- Helper-returning methods ---- + + private AsyncMapIterator Map(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "map", out var mapper); + return new AsyncMapIterator(_engine, GetIteratorDirect(o), mapper); + } + + private AsyncFilterIterator Filter(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "filter", out var predicate); + return new AsyncFilterIterator(_engine, GetIteratorDirect(o), predicate); + } + + private AsyncTakeIterator Take(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetLimit(thisObject, arguments, "take", out var limit); + return new AsyncTakeIterator(_engine, GetIteratorDirect(o), limit); + } + + private AsyncDropIterator Drop(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetLimit(thisObject, arguments, "drop", out var limit); + return new AsyncDropIterator(_engine, GetIteratorDirect(o), limit); + } + + private AsyncFlatMapIterator FlatMap(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "flatMap", out var mapper); + return new AsyncFlatMapIterator(_engine, GetIteratorDirect(o), mapper); + } + + // ---- Consuming methods (return promises) ---- + + private JsValue Reduce(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "reduce", out var reducer); + var iterated = GetIteratorDirect(o); + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + var hasInitialValue = arguments.Length >= 2; + var accumulator = hasInitialValue ? arguments.At(1) : Undefined; + + AsyncReduceLoop(iterated, reducer, accumulator, hasInitialValue, 0, promiseCapability); + return promiseCapability.PromiseInstance; + } + + private void AsyncReduceLoop( + IteratorInstance.ObjectIterator iterated, + ICallable reducer, + JsValue accumulator, + bool hasAccumulator, + int counter, + PromiseCapability promiseCapability) + { + var capturedAccumulator = accumulator; + var capturedHasAccumulator = hasAccumulator; + + AsyncIterateLoop(iterated, promiseCapability, + onDone: (_, cap) => + { + if (!capturedHasAccumulator) + { + cap.Reject.Call(Undefined, new JsValue[] { _engine.Realm.Intrinsics.TypeError.Construct("Reduce of empty iterator with no initial value") }); + } + else + { + cap.Resolve.Call(Undefined, new[] { capturedAccumulator }); + } + }, + onValue: (value, idx, cap) => + { + if (!capturedHasAccumulator) + { + AsyncReduceLoop(iterated, reducer, value, true, idx + 1, cap); + return; + } + + JsValue newAccumulator; + try + { + newAccumulator = reducer.Call(Undefined, new[] { capturedAccumulator, value, (JsValue) idx }); + } + catch (JavaScriptException ex) + { + try { iterated.Close(CompletionType.Throw); } catch { /* ignore */ } + cap.Reject.Call(Undefined, new[] { ex.Error }); + return; + } + + ChainCallbackResult(iterated, newAccumulator, cap, resolved => + { + AsyncReduceLoop(iterated, reducer, resolved, true, idx + 1, cap); + }); + }, + counter: counter); + } + + private JsValue ToArray(JsValue thisObject, JsValue[] arguments) + { + if (thisObject is not ObjectInstance o) + { + Throw.TypeError(_realm, "AsyncIterator.prototype.toArray called on non-object"); + return Undefined; + } + + var iterated = GetIteratorDirect(o); + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + var items = new List(); + + AsyncCollectLoop(iterated, items, promiseCapability); + return promiseCapability.PromiseInstance; + } + + private void AsyncCollectLoop( + IteratorInstance.ObjectIterator iterated, + List items, + PromiseCapability promiseCapability) + { + AsyncIterateLoop(iterated, promiseCapability, + onDone: (_, cap) => + { + var array = new JsArray(_engine, items.ToArray()); + cap.Resolve.Call(Undefined, new JsValue[] { array }); + }, + onValue: (value, _, _) => + { + items.Add(value); + AsyncCollectLoop(iterated, items, promiseCapability); + }); + } + + private JsValue ForEach(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "forEach", out var procedure); + var iterated = GetIteratorDirect(o); + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + AsyncConsumeLoop(iterated, procedure, promiseCapability, + onDone: cap => cap.Resolve.Call(Undefined, Undefined), + shouldStop: null); + + return promiseCapability.PromiseInstance; + } + + private JsValue Some(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "some", out var predicate); + var iterated = GetIteratorDirect(o); + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + AsyncConsumeLoop(iterated, predicate, promiseCapability, + onDone: cap => cap.Resolve.Call(Undefined, new JsValue[] { JsBoolean.False }), + shouldStop: (resolved, value, cap) => + { + if (!TypeConverter.ToBoolean(resolved)) return false; + try { iterated.Close(CompletionType.Normal); } catch { /* ignore */ } + cap.Resolve.Call(Undefined, new JsValue[] { JsBoolean.True }); + return true; + }); + + return promiseCapability.PromiseInstance; + } + + private JsValue Every(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "every", out var predicate); + var iterated = GetIteratorDirect(o); + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + AsyncConsumeLoop(iterated, predicate, promiseCapability, + onDone: cap => cap.Resolve.Call(Undefined, new JsValue[] { JsBoolean.True }), + shouldStop: (resolved, value, cap) => + { + if (TypeConverter.ToBoolean(resolved)) return false; + try { iterated.Close(CompletionType.Normal); } catch { /* ignore */ } + cap.Resolve.Call(Undefined, new JsValue[] { JsBoolean.False }); + return true; + }); + + return promiseCapability.PromiseInstance; + } + + private JsValue Find(JsValue thisObject, JsValue[] arguments) + { + var o = ValidateThisAndGetCallable(thisObject, arguments, "find", out var predicate); + var iterated = GetIteratorDirect(o); + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise); + + AsyncConsumeLoop(iterated, predicate, promiseCapability, + onDone: cap => cap.Resolve.Call(Undefined, Undefined), + shouldStop: (resolved, value, cap) => + { + if (!TypeConverter.ToBoolean(resolved)) return false; + try { iterated.Close(CompletionType.Normal); } catch { /* ignore */ } + cap.Resolve.Call(Undefined, new[] { value }); + return true; + }); + + return promiseCapability.PromiseInstance; + } + + /// + /// Shared async consumption loop for forEach/some/every/find. + /// Calls the callback for each value, chains on its result. If shouldStop returns true, + /// the loop terminates (shouldStop is responsible for resolving). Otherwise continues. + /// + private void AsyncConsumeLoop( + IteratorInstance.ObjectIterator iterated, + ICallable callback, + PromiseCapability promiseCapability, + Action onDone, + Func? shouldStop, + int counter = 0) + { + AsyncIterateLoop(iterated, promiseCapability, + onDone: (_, cap) => onDone(cap), + onValue: (value, idx, cap) => + { + JsValue callResult; + try + { + callResult = callback.Call(Undefined, new[] { value, (JsValue) idx }); + } + catch (JavaScriptException ex) + { + try { iterated.Close(CompletionType.Throw); } catch { /* ignore */ } + cap.Reject.Call(Undefined, new[] { ex.Error }); + return; + } + + ChainCallbackResult(iterated, callResult, cap, resolved => + { + if (shouldStop is not null && shouldStop(resolved, value, cap)) + { + return; + } + + AsyncConsumeLoop(iterated, callback, cap, onDone, shouldStop, idx + 1); + }); + }, + counter: counter); + } } diff --git a/Jint/Runtime/Intrinsics.cs b/Jint/Runtime/Intrinsics.cs index 6fd6636d7d..88a52125d3 100644 --- a/Jint/Runtime/Intrinsics.cs +++ b/Jint/Runtime/Intrinsics.cs @@ -73,6 +73,9 @@ public sealed partial class Intrinsics private IteratorHelperPrototype? _iteratorHelperPrototype; private WrapForValidIteratorPrototype? _wrapForValidIteratorPrototype; private AsyncIteratorPrototype? _asyncIteratorPrototype; + private AsyncIteratorConstructor? _asyncIteratorConstructor; + private AsyncIteratorHelperPrototype? _asyncIteratorHelperPrototype; + private WrapForValidAsyncIteratorPrototype? _wrapForValidAsyncIteratorPrototype; private AsyncFromSyncIteratorPrototype? _asyncFromSyncIteratorPrototype; private MathInstance? _math; private JsonInstance? _json; @@ -244,6 +247,15 @@ internal Intrinsics(Engine engine, Realm realm) internal AsyncIteratorPrototype AsyncIteratorPrototype => _asyncIteratorPrototype ??= new AsyncIteratorPrototype(_engine, _realm, Object.PrototypeObject); + internal AsyncIteratorConstructor AsyncIterator => + _asyncIteratorConstructor ??= new AsyncIteratorConstructor(_engine, _realm, Function.PrototypeObject, AsyncIteratorPrototype); + + internal AsyncIteratorHelperPrototype AsyncIteratorHelperPrototype => + _asyncIteratorHelperPrototype ??= new AsyncIteratorHelperPrototype(_engine, _realm, AsyncIteratorPrototype); + + internal WrapForValidAsyncIteratorPrototype WrapForValidAsyncIteratorPrototype => + _wrapForValidAsyncIteratorPrototype ??= new WrapForValidAsyncIteratorPrototype(_engine, _realm, AsyncIteratorPrototype); + internal AsyncFromSyncIteratorPrototype AsyncFromSyncIteratorPrototype => _asyncFromSyncIteratorPrototype ??= new AsyncFromSyncIteratorPrototype(_engine, _realm, AsyncIteratorPrototype);