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 ES2021 Promise.any #1610

Merged
merged 2 commits into from
Sep 12, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 57 additions & 6 deletions rhino/src/main/java/org/mozilla/javascript/NativeError.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package org.mozilla.javascript;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
* The class of error objects
Expand Down Expand Up @@ -55,12 +57,7 @@ static NativeError make(Context cx, Scriptable scope, IdFunctionObject ctorObj,
}
if (arglen >= 2) {
if (args[1] instanceof NativeObject) {
NativeObject options = (NativeObject) args[1];
Object cause = ScriptableObject.getProperty(options, "cause");
if (cause != NOT_FOUND) {
ScriptableObject.putProperty(obj, "cause", cause);
obj.setAttributes("cause", DONTENUM);
}
installCause((NativeObject) args[1], obj);
} else {
ScriptableObject.putProperty(obj, "fileName", ScriptRuntime.toString(args[1]));
if (arglen >= 3) {
Expand All @@ -73,6 +70,60 @@ static NativeError make(Context cx, Scriptable scope, IdFunctionObject ctorObj,
return obj;
}

static NativeError makeAggregate(
Context cx, Scriptable scope, IdFunctionObject ctorObj, Object[] args) {
Scriptable proto = (Scriptable) ctorObj.get("prototype", ctorObj);

NativeError obj = new NativeError();
obj.setPrototype(proto);
obj.setParentScope(scope);

int arglen = args.length;
if (arglen >= 1) {
if (arglen >= 2) {
if (!Undefined.isUndefined(args[1])) {
ScriptableObject.putProperty(obj, "message", ScriptRuntime.toString(args[1]));
obj.setAttributes("message", DONTENUM);
}

if (arglen >= 3) {
if (args[2] instanceof NativeObject) {
installCause((NativeObject) args[2], obj);
} else {
ScriptableObject.putProperty(
obj, "fileName", ScriptRuntime.toString(args[2]));
if (arglen >= 4) {
ScriptableObject.putProperty(
obj, "lineNumber", ScriptRuntime.toInt32(args[3]));
}
}
}
}

final Object iterator = ScriptRuntime.callIterator(args[0], cx, scope);
try (IteratorLikeIterable it = new IteratorLikeIterable(cx, scope, iterator)) {
List<Object> errors = new ArrayList<>();
for (Object o : it) {
errors.add(o);
}

Scriptable newArray = cx.newArray(scope, errors.toArray());
obj.defineProperty("errors", newArray, DONTENUM);
}
} else {
throw ScriptRuntime.typeErrorById("msg.iterable.expected");
}
return obj;
}

static void installCause(NativeObject options, NativeError obj) {
Object cause = ScriptableObject.getProperty(options, "cause");
if (cause != NOT_FOUND) {
ScriptableObject.putProperty(obj, "cause", cause);
obj.setAttributes("cause", DONTENUM);
}
}

@Override
protected void fillConstructorProperties(IdFunctionObject ctor) {
addIdFunctionProperty(
Expand Down
14 changes: 11 additions & 3 deletions rhino/src/main/java/org/mozilla/javascript/NativeGlobal.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ public static void init(Context cx, Scriptable scope, boolean sealed) {
cx, scope, TopLevel.Builtins.Error, ScriptRuntime.emptyArgs);
errorProto.defineProperty("name", name, DONTENUM);
errorProto.defineProperty("message", "", DONTENUM);
IdFunctionObject ctor =
new IdFunctionObject(obj, FTAG, Id_new_CommonError, name, 1, scope);
IdFunctionObject ctor;
if (error == TopLevel.NativeErrors.AggregateError) {
ctor = new IdFunctionObject(obj, FTAG, Id_new_AggregateError, name, 2, scope);
} else {
ctor = new IdFunctionObject(obj, FTAG, Id_new_CommonError, name, 1, scope);
}
ctor.markAsConstructor(errorProto);
ctor.setPrototype(nativeError);
errorProto.put("constructor", errorProto, ctor);
Expand Down Expand Up @@ -203,6 +207,9 @@ public Object execIdCall(
// The implementation of all the ECMA error constructors
// (SyntaxError, TypeError, etc.)
return NativeError.make(cx, scope, f, args);

case Id_new_AggregateError:
return NativeError.makeAggregate(cx, scope, f, args);
}
}
throw f.unknown();
Expand Down Expand Up @@ -760,5 +767,6 @@ private static int oneUcs4ToUtf8Char(byte[] utf8Buffer, int ucs4Char) {
Id_unescape = 12,
Id_uneval = 13,
LAST_SCOPE_FUNCTION_ID = 13,
Id_new_CommonError = 14;
Id_new_CommonError = 14,
Id_new_AggregateError = 15;
}
153 changes: 153 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/NativePromise.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public static void init(Context cx, Scriptable scope, boolean sealed) {
scope, "allSettled", 1, NativePromise::allSettled, DONTENUM, DONTENUM | READONLY);
constructor.defineConstructorMethod(
scope, "race", 1, NativePromise::race, DONTENUM, DONTENUM | READONLY);
constructor.defineConstructorMethod(
scope, "any", 1, NativePromise::any, DONTENUM, DONTENUM | READONLY);

ScriptRuntimeES6.addSymbolSpecies(cx, scope, constructor);

Expand Down Expand Up @@ -280,6 +282,43 @@ private static Object performRace(
}
}

private static Object any(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Capability cap = new Capability(cx, scope, thisObj);
Object arg = (args.length > 0 ? args[0] : Undefined.instance);

IteratorLikeIterable iterable;
try {
Object maybeIterable = ScriptRuntime.callIterator(arg, cx, scope);
iterable = new IteratorLikeIterable(cx, scope, maybeIterable);
} catch (RhinoException re) {
cap.reject.call(
cx,
scope,
Undefined.SCRIPTABLE_UNDEFINED,
new Object[] {getErrorObject(cx, scope, re)});
return cap.promise;
}

IteratorLikeIterable.Itr iterator = iterable.iterator();
try {
PromiseAnyRejector rejector = new PromiseAnyRejector(iterator, thisObj, cap);
try {
return rejector.reject(cx, scope);
} finally {
if (!iterator.isDone()) {
iterable.close();
}
}
} catch (RhinoException re) {
cap.reject.call(
cx,
scope,
Undefined.SCRIPTABLE_UNDEFINED,
new Object[] {getErrorObject(cx, scope, re)});
return cap.promise;
}
}

// Promise.prototype.then
private Object then(
Context cx, Scriptable scope, LambdaConstructor defaultConstructor, Object[] args) {
Expand Down Expand Up @@ -811,7 +850,109 @@ void finalResolution(Context cx, Scriptable scope) {
}
}

// This object keeps track of the state necessary to execute Promise.any
private static class PromiseAnyRejector {
// Limit the number of promises in Promise.any the same as it is in V8.
private static final int MAX_PROMISES = 1 << 21;

final ArrayList<Object> errors = new ArrayList<>();
int remainingElements = 1;

IteratorLikeIterable.Itr iterator;
Scriptable thisObj;
Capability capability;

PromiseAnyRejector(IteratorLikeIterable.Itr iter, Scriptable thisObj, Capability cap) {
this.iterator = iter;
this.thisObj = thisObj;
this.capability = cap;
}

Object reject(Context topCx, Scriptable topScope) {
int index = 0;
// Do this first because we should catch any exception before
// invoking the iterator.
Callable resolve =
ScriptRuntime.getPropFunctionAndThis(thisObj, "resolve", topCx, topScope);
Scriptable storedThis = ScriptRuntime.lastStoredScriptable(topCx);

// Iterate manually because we need to catch exceptions in a special way.
while (true) {
if (index == MAX_PROMISES) {
throw ScriptRuntime.rangeErrorById("msg.promise.any.toobig");
}
boolean hasNext;
Object nextVal = Undefined.instance;
boolean nextOk = false;
try {
hasNext = iterator.hasNext();
if (hasNext) {
nextVal = iterator.next();
}
nextOk = true;
} finally {
if (!nextOk) {
iterator.setDone(true);
}
}

if (!hasNext) {
if (--remainingElements == 0) {
Scriptable newArray = topCx.newArray(topScope, errors.toArray());
NativeError error =
(NativeError)
topCx.newObject(
topScope,
"AggregateError",
new Object[] {newArray});
throw new JavaScriptException(error, null, 0);
}
return capability.promise;
}

errors.add(Undefined.instance);

// Call "resolve" to get the next promise in the chain
Object nextPromise =
resolve.call(topCx, topScope, storedThis, new Object[] {nextVal});

// Create a resolution func that will stash its result in the right place
PromiseElementResolver eltResolver = new PromiseElementResolver(index);
LambdaFunction rejectFunc =
new LambdaFunction(
topScope,
1,
(Context cx,
Scriptable scope,
Scriptable thisObj,
Object[] args) -> {
Object value = (args.length > 0 ? args[0] : Undefined.instance);
return eltResolver.reject(cx, scope, value, this);
});
remainingElements++;

// Call "then" on the promise with the resolution func
Callable thenFunc =
ScriptRuntime.getPropFunctionAndThis(nextPromise, "then", topCx, topScope);
thenFunc.call(
topCx,
topScope,
ScriptRuntime.lastStoredScriptable(topCx),
new Object[] {capability.resolve, rejectFunc});
index++;
}
}

void finalRejection(Context cx, Scriptable scope) {
Scriptable newArray = cx.newArray(scope, errors.toArray());
NativeError error =
(NativeError) cx.newObject(scope, "AggregateError", new Object[] {newArray});
capability.reject.call(cx, scope, Undefined.SCRIPTABLE_UNDEFINED, new Object[] {error});
}
}

// This object keeps track of the state necessary to resolve one element in Promise.all
// and Promise.any
private static class PromiseElementResolver {

private boolean alreadyCalled = false;
Expand All @@ -832,5 +973,17 @@ Object resolve(Context cx, Scriptable scope, Object result, PromiseAllResolver r
}
return Undefined.instance;
}

Object reject(Context cx, Scriptable scope, Object result, PromiseAnyRejector rejector) {
if (alreadyCalled) {
return Undefined.instance;
}
alreadyCalled = true;
rejector.errors.set(index, result);
if (--rejector.remainingElements == 0) {
rejector.finalRejection(cx, scope);
}
return Undefined.instance;
}
}
}
2 changes: 2 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/TopLevel.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public enum Builtins {

/** An enumeration of built-in native errors. [ECMAScript 5 - 15.11.6] */
enum NativeErrors {
/** The AggregateError */
AggregateError,
/** Basic Error */
Error,
/** The native EvalError. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,5 +964,12 @@ msg.promise.capability.state =\
msg.promise.all.toobig =\
Too many inputs to Promise.all

msg.promise.any.toobig =\
Too many inputs to Promise.any

msg.typed.array.ctor.incompatible = \
Method %TypedArray%.prototype.{0} called on incompatible receiver

# NativeError
msg.iterable.expected =\
Expected the first argument to be iterable
Loading
Loading