Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Promise.any #1312

Merged
merged 1 commit into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"generators",
"import-assertions",
"Promise.allSettled",
"Promise.any",
"regexp-duplicate-named-groups",
"regexp-lookbehind",
"regexp-unicode-property-escapes",
Expand Down
196 changes: 163 additions & 33 deletions Jint/Native/Promise/PromiseConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ internal sealed record PromiseCapability(
JsValue PromiseInstance,
ICallable Resolve,
ICallable Reject,
JsValue RejectObj
JsValue RejectObj,
JsValue ResolveObj
);

public sealed class PromiseConstructor : FunctionInstance, IConstructor
Expand Down Expand Up @@ -49,6 +50,8 @@ protected override void Initialize()
propertyFlags)),
["all"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "all", All, 1, lengthFlags),
propertyFlags)),
["any"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "any", Any, 1, lengthFlags),
propertyFlags)),
["race"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "race", Race, 1, lengthFlags),
propertyFlags)),
};
Expand Down Expand Up @@ -89,7 +92,7 @@ ObjectInstance IConstructor.Construct(JsValue[] arguments, JsValue newTarget)
static (Engine engine, Realm _, object? _) => new PromiseInstance(engine));

var (resolve, reject) = instance.CreateResolvingFunctions();
promiseExecutor.Call(Undefined, new JsValue[] {resolve, reject});
promiseExecutor.Call(Undefined, new JsValue[] { resolve, reject });

return instance;
}
Expand Down Expand Up @@ -126,9 +129,9 @@ internal JsValue Resolve(JsValue thisObj, JsValue[] arguments)
}
}

var (instance, resolve, _, _) = NewPromiseCapability(_engine, thisObj);
var (instance, resolve, _, _, _) = NewPromiseCapability(_engine, thisObj);

resolve.Call(Undefined, new[] {x});
resolve.Call(Undefined, new[] { x });

return instance;
}
Expand All @@ -147,9 +150,9 @@ private JsValue Reject(JsValue thisObj, JsValue[] arguments)

var r = arguments.At(0);

var (instance, _, reject, _) = NewPromiseCapability(_engine, thisObj);
var (instance, _, reject, _, _) = NewPromiseCapability(_engine, thisObj);

reject.Call(Undefined, new[] {r});
reject.Call(Undefined, new[] { r });

return instance;
}
Expand All @@ -160,14 +163,14 @@ private JsValue Reject(JsValue thisObj, JsValue[] arguments)
//
// 1. Let C be the this value.
// 2. Let promiseCapability be ? NewPromiseCapability(C).
// 3. Let promiseResolve be GetPromiseResolve(C).
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
// 5. Let iteratorRecord be GetIterator(iterable).
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
// 7. Let result be PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve).
// 8. If result is an abrupt completion, then
// 3. Let promiseResolve be GetPromiseResolve(C).
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
// 5. Let iteratorRecord be GetIterator(iterable).
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
// 7. Let result be PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve).
// 8. If result is an abrupt completion, then
// a. If iteratorRecord.[[Done]] is false, set result to IteratorClose(iteratorRecord, result).
// b. IfAbruptRejectPromise(result, promiseCapability).
// b. IfAbruptRejectPromise(result, promiseCapability).
// 9. Return Completion(result)
private JsValue All(JsValue thisObj, JsValue[] arguments)
{
Expand All @@ -177,7 +180,7 @@ private JsValue All(JsValue thisObj, JsValue[] arguments)
}

//2. Let promiseCapability be ? NewPromiseCapability(C).
var (resultingPromise, resolve, reject, rejectObj) = NewPromiseCapability(_engine, thisObj);
var (resultingPromise, resolve, reject, _, rejectObj) = NewPromiseCapability(_engine, thisObj);

//3. Let promiseResolve be GetPromiseResolve(C).
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
Expand All @@ -188,7 +191,7 @@ private JsValue All(JsValue thisObj, JsValue[] arguments)
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

Expand All @@ -210,7 +213,7 @@ private JsValue All(JsValue thisObj, JsValue[] arguments)
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

Expand Down Expand Up @@ -253,7 +256,7 @@ void ResolveIfFinished()
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

Expand All @@ -262,7 +265,7 @@ void ResolveIfFinished()
// In F# it would be Option<JsValue>
results.Add(null!);

var item = promiseResolve.Call(thisObj, new JsValue[] {value});
var item = promiseResolve.Call(thisObj, new JsValue[] { value });
var thenProps = item.Get("then");
if (thenProps is ICallable thenFunc)
{
Expand All @@ -282,7 +285,7 @@ void ResolveIfFinished()
return Undefined;
}, 1, PropertyFlag.Configurable);

thenFunc.Call(item, new JsValue[] {onSuccess, rejectObj});
thenFunc.Call(item, new JsValue[] { onSuccess, rejectObj });
}
else
{
Expand All @@ -295,16 +298,143 @@ void ResolveIfFinished()
catch (JavaScriptException e)
{
iterator.Close(CompletionType.Throw);
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

// if there were not items but the iteration was successful
// e.g. "[]" empty array as an example
// resolve the promise sync
Comment on lines -302 to -304
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this from Promise.all because ResolveIfFinished already does that

if (results.Count == 0)
return resultingPromise;
}

// https://tc39.es/ecma262/#sec-promise.any
private JsValue Any(JsValue thisObj, JsValue[] arguments)
{
if (!thisObj.IsObject())
{
ExceptionHelper.ThrowTypeError(_realm, "Promise.any called on non-object");
}

//2. Let promiseCapability be ? NewPromiseCapability(C).
var (resultingPromise, resolve, reject, resolveObj, _) = NewPromiseCapability(_engine, thisObj);

//3. Let promiseResolve be GetPromiseResolve(C).
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
ICallable promiseResolve;
try
{
resolve.Call(Undefined, new JsValue[] {_realm.Intrinsics.Array.ArrayCreate(0)});
promiseResolve = GetPromiseResolve(thisObj);
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}


IteratorInstance iterator;
// 5. Let iteratorRecord be GetIterator(iterable).
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).

try
{
if (arguments.Length == 0)
{
ExceptionHelper.ThrowTypeError(_realm, "no arguments were passed to Promise.all");
}

var iterable = arguments.At(0);

iterator = iterable.GetIterator(_realm);
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

var errors = new List<JsValue>();
bool doneIterating = false;

void RejectIfAllRejected()
{
// that means all of them were rejected
// Note that "Undefined" is not null, thus the logic is sound, even though awkward
// also note that it is important to check if we are done iterating.
// if "then" method is sync then it will be resolved BEFORE the next iteration cycle

if (errors.TrueForAll(static x => x != null) && doneIterating)
{
var array = _realm.Intrinsics.Array.ConstructFast(errors);

reject.Call(Undefined, new JsValue[] { Construct(_realm.Intrinsics.AggregateError, new JsValue[] { array }) });
}
}

// https://tc39.es/ecma262/#sec-performpromiseany
try
{
int index = 0;

do
{
JsValue value;
try
{
if (!iterator.TryIteratorStep(out var nextItem))
{
doneIterating = true;
RejectIfAllRejected();
break;
}

value = nextItem.Get(CommonProperties.Value);
}
catch (JavaScriptException e)
{
errors.Add(e.Error);
continue;
}

// note that null here is important
// it will help to detect if all inner promises were rejected
// In F# it would be Option<JsValue>
errors.Add(null!);

var item = promiseResolve.Call(thisObj, new JsValue[] { value });
var thenProps = item.Get("then");
if (thenProps is ICallable thenFunc)
{
var capturedIndex = index;

var fulfilled = false;

var onError =
new ClrFunctionInstance(_engine, "", (_, args) =>
{
if (!fulfilled)
{
fulfilled = true;
errors[capturedIndex] = args.At(0);
RejectIfAllRejected();
}

return Undefined;
}, 1, PropertyFlag.Configurable);

thenFunc.Call(item, new JsValue[] { resolveObj, onError });
}
else
{
ExceptionHelper.ThrowTypeError(_realm, "Passed non Promise-like value");
}

index += 1;
} while (true);
}
catch (JavaScriptException e)
{
iterator.Close(CompletionType.Throw);
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

return resultingPromise;
Expand All @@ -319,7 +449,7 @@ private JsValue Race(JsValue thisObj, JsValue[] arguments)
}

// 2. Let promiseCapability be ? NewPromiseCapability(C).
var (resultingPromise, resolve, reject, rejectObj) = NewPromiseCapability(_engine, thisObj);
var (resultingPromise, resolve, reject, _, rejectObj) = NewPromiseCapability(_engine, thisObj);

// 3. Let promiseResolve be GetPromiseResolve(C).
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
Expand All @@ -330,7 +460,7 @@ private JsValue Race(JsValue thisObj, JsValue[] arguments)
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

Expand All @@ -352,7 +482,7 @@ private JsValue Race(JsValue thisObj, JsValue[] arguments)
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

Expand All @@ -374,12 +504,12 @@ private JsValue Race(JsValue thisObj, JsValue[] arguments)
}
catch (JavaScriptException e)
{
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

// h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
var nextPromise = promiseResolve.Call(thisObj, new JsValue[] {nextValue});
var nextPromise = promiseResolve.Call(thisObj, new JsValue[] { nextValue });

// i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »).

Expand All @@ -392,7 +522,7 @@ private JsValue Race(JsValue thisObj, JsValue[] arguments)
// a. If iteratorRecord.[[Done]] is false, set result to IteratorClose(iteratorRecord, result).
// b. IfAbruptRejectPromise(result, promiseCapability).
iterator.Close(CompletionType.Throw);
reject.Call(Undefined, new[] {e.Error});
reject.Call(Undefined, new[] { e.Error });
return resultingPromise;
}

Expand Down Expand Up @@ -472,7 +602,7 @@ JsValue Executor(JsValue thisObj, JsValue[] arguments)

var executor = new ClrFunctionInstance(engine, "", Executor, 2, PropertyFlag.Configurable);

var instance = ctor.Construct(new JsValue[] {executor}, c);
var instance = ctor.Construct(new JsValue[] { executor }, c);

ICallable? resolve = null;
ICallable? reject = null;
Expand All @@ -495,7 +625,7 @@ JsValue Executor(JsValue thisObj, JsValue[] arguments)
ExceptionHelper.ThrowTypeError(engine.Realm, "reject is not a function");
}

return new PromiseCapability(instance, resolve, reject, rejectArg);
return new PromiseCapability(instance, resolve, reject, resolveArg, rejectArg);
}
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The entire execution engine was rebuild with performance in mind, in many cases
- ✔ Logical Assignment Operators (`&&=` `||=` `??=`)
- ✔ Numeric Separators (`1_000`)
- ✔ `AggregateError`
- `Promise.any`
- `Promise.any`
- ✔ `String.prototype.replaceAll`
- ✔ `WeakRef`
- ❌ `FinalizationRegistry`
Expand Down