Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
227 changes: 227 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/NativeArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ static void init(Context cx, Scriptable scope, boolean sealed) {

defineMethodOnConstructor(ctor, scope, "of", 0, NativeArray::js_of);
defineMethodOnConstructor(ctor, scope, "from", 1, NativeArray::js_from);
defineMethodOnConstructor(ctor, scope, "fromAsync", 1, NativeArray::js_fromAsync);
defineMethodOnConstructor(ctor, scope, "isArray", 1, NativeArray::js_isArrayMethod);

// The following need to appear on the constructor for
Expand Down Expand Up @@ -748,6 +749,232 @@ private static Object js_from(Context cx, Scriptable scope, Scriptable thisObj,
return result;
}

private static Object js_fromAsync(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// Get the Promise constructor
Object promiseCtor = ScriptableObject.getProperty(scope, "Promise");
if (!(promiseCtor instanceof Function)) {
throw ScriptRuntime.typeErrorById("msg.no.promise");
}
Function promiseFunc = (Function) promiseCtor;

// Create executor for the Promise
AsyncFromExecutor executor = new AsyncFromExecutor(thisObj, args);
return promiseFunc.construct(cx, scope, new Object[] {executor});
Copy link
Contributor

Choose a reason for hiding this comment

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

The promise constructor only requires that its argument is a Callable, so you could just pass method reference rather than use a whole separate subclass of BaseFunction here, but I wonder if it would be better hold off a little on this and work out how we want to represent async built in functions, there are bound to be far more added to the spec over the next few years.

I’m also not convinced this algorithm matches the draft spec, it’s not clear to me that it has the right behaviour in error cases, but I’ll need to find time to sit down with this, the test cases, and the draft spec to be sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aardvark179 You're absolutely right on all three points.

  1. BaseFunction vs Callable

Agreed - using BaseFunction subclasses is unnecessary overhead. LambdaFunction or direct Callable is the correct pattern, as established in NativePromise (Promise.try, Promise.finally).

  1. Sequential vs Parallel Processing

The Promise.all approach is fundamentally wrong. I used it for simplicity given Rhino's lack of async/await, but it violates the ES2024 spec which requires sequential processing like for await...of. This affects execution order, error handling, and resource consumption.

  1. Error Handling Non-compliance

Current implementation doesn't match spec - it processes all values before handling errors, when it should stop at the first error (lazy) evaluation).

Why This Approach Was Taken

Without async/await or for-await-of in Rhino, I opted for Promise.all as the path of least resistance. This was a mistake - correctness should trump implementation simplicity.

Other approach is replacing Promise.all with recursive promise chaining:, Replace both AsyncFromExecutor and AsyncFromHandler with LambdaFunction.

Given the complexity of implementing ES2024 async semantics without async/await, Which of these following approach would you prefer ?

A: Fix now with correct but complex manual promise chainingOption
B: Create the reusable async pattern first, then apply it hereOption
C:Defer until Rhino has broader async support

Copy link
Contributor

Choose a reason for hiding this comment

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

So, this proposal is still in draft, so I see no strong need to rush to get an implementation in, and therefore I don’t think we should do A. I think B or C are probably the options to go for, with B seeming quite approachable, but using it as a lever to get us closer to C.

So, what would an async pattern for built-ins look like? I think, given this function is async because it must itself wait for results, then a generator returning promises that use then to trigger the next step would work well, and so I think the very first step is to be able to expresss a generator.

I think the model we currently for compiled generators (numbered re-entry points and a mutable object used to capture variable state) could be adapted nicely for asynchronous built-in functions, but if we are going to do some refactoring of that code I’d love to see us unify and reduce our generator implementations rather than add new ones.

If you want to try this then try refactoring the interpreter and compiler generator implementations to share the same model, and we can see how close that gets us. It will be hard on master at the moment, but #2091 goes some way towards this and might be easier to build upon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aardvark179 You are right. waiting for PR #2091 to land would provide a much better foundation

}

// Helper class to handle Promise executor logic
private static class AsyncFromExecutor extends BaseFunction {
private final Scriptable constructorThis;
private final Object[] fromAsyncArgs;

AsyncFromExecutor(Scriptable constructorThis, Object[] fromAsyncArgs) {
this.constructorThis = constructorThis;
this.fromAsyncArgs = fromAsyncArgs;
}

@Override
public Object call(
Context cx, Scriptable scope, Scriptable thisObj, Object[] executorArgs) {
if (executorArgs.length < 2) {
return Undefined.instance;
}
Function resolve = (Function) executorArgs[0];
Function reject = (Function) executorArgs[1];

try {
// Extract arguments
Object items = (fromAsyncArgs.length >= 1) ? fromAsyncArgs[0] : Undefined.instance;
Object mapArg = (fromAsyncArgs.length >= 2) ? fromAsyncArgs[1] : Undefined.instance;
Object mapperThisArg =
(fromAsyncArgs.length >= 3)
? fromAsyncArgs[2]
: Undefined.SCRIPTABLE_UNDEFINED;

// Validate and prepare mapper
Function mapFn = null;
Scriptable mapThisArg = null;
if (!Undefined.isUndefined(mapArg)) {
if (!(mapArg instanceof Function)) {
throw ScriptRuntime.typeErrorById("msg.map.function.not");
}
mapFn = (Function) mapArg;
mapThisArg =
ScriptRuntime.getApplyOrCallThis(cx, scope, mapperThisArg, 1, mapFn);
}

// Process the async iteration
processAsyncIteration(
cx, scope, constructorThis, items, mapFn, mapThisArg, resolve, reject);

} catch (RhinoException e) {
reject.call(cx, scope, null, new Object[] {e});
}

return Undefined.instance;
}

@Override
public Scriptable construct(Context cx, Scriptable scope, Object[] args) {
throw ScriptRuntime.typeErrorById("msg.not.ctor", "AsyncFromExecutor");
}
}

private static void processAsyncIteration(
Context cx,
Scriptable scope,
Scriptable thisObj,
Object items,
Function mapFn,
Scriptable thisArg,
Function resolve,
Function reject) {

try {
Scriptable itemsObj = ScriptRuntime.toObject(scope, items);

// Check for Symbol.iterator
Object iteratorProp = ScriptableObject.getProperty(itemsObj, SymbolKey.ITERATOR);

if ((iteratorProp != Scriptable.NOT_FOUND) && !Undefined.isUndefined(iteratorProp)) {
// Handle iterable case
Object iterator = ScriptRuntime.callIterator(itemsObj, cx, scope);
if (!Undefined.isUndefined(iterator)) {
Scriptable result = callConstructorOrCreateArray(cx, scope, thisObj, 0, false);

// Collect all values into promises
List<Object> promises = new ArrayList<>();

try (IteratorLikeIterable it = new IteratorLikeIterable(cx, scope, iterator)) {
for (Object value : it) {
// Convert each value to a promise
Object promiseValue = promiseResolve(cx, scope, value);
promises.add(promiseValue);
}
}

// Use Promise.all to await all values
awaitAllAndFinish(cx, scope, result, promises, mapFn, thisArg, resolve);
return;
}
}

// Handle array-like case
long length = getLengthProperty(cx, itemsObj);
Scriptable result = callConstructorOrCreateArray(cx, scope, thisObj, length, true);

List<Object> promises = new ArrayList<>();
for (long k = 0; k < length; k++) {
Object value = getElem(cx, itemsObj, k);
Object promiseValue = promiseResolve(cx, scope, value);
promises.add(promiseValue);
}

// Use Promise.all to await all values
awaitAllAndFinish(cx, scope, result, promises, mapFn, thisArg, resolve);

} catch (RhinoException e) {
reject.call(cx, scope, null, new Object[] {e});
}
}

private static Object promiseResolve(Context cx, Scriptable scope, Object value) {
Object promiseCtor = ScriptableObject.getProperty(scope, "Promise");
if (promiseCtor instanceof Function) {
Function promise = (Function) promiseCtor;
Object resolveMethod =
ScriptableObject.getProperty((Scriptable) promiseCtor, "resolve");
if (resolveMethod instanceof Function) {
return ((Function) resolveMethod)
.call(cx, scope, (Scriptable) promiseCtor, new Object[] {value});
}
}
return value;
}

private static void awaitAllAndFinish(
Context cx,
Scriptable scope,
Scriptable result,
List<Object> promises,
Function mapFn,
Scriptable thisArg,
Function resolve) {

Object promiseCtor = ScriptableObject.getProperty(scope, "Promise");
if (!(promiseCtor instanceof Function)) {
return;
}

Object allMethod = ScriptableObject.getProperty((Scriptable) promiseCtor, "all");
if (!(allMethod instanceof Function)) {
return;
}

// Create array from promises
Scriptable promiseArray = cx.newArray(scope, promises.toArray());

// Call Promise.all
Object allPromise =
((Function) allMethod)
.call(cx, scope, (Scriptable) promiseCtor, new Object[] {promiseArray});

// Chain with then to process results
if (!(allPromise instanceof Scriptable)) {
return;
}

Object thenMethod = ScriptableObject.getProperty((Scriptable) allPromise, "then");
if (!(thenMethod instanceof Function)) {
return;
}

// Create handler for Promise.all resolution
AsyncFromHandler handler = new AsyncFromHandler(result, mapFn, thisArg, resolve);
((Function) thenMethod).call(cx, scope, (Scriptable) allPromise, new Object[] {handler});
}

// Helper class to handle Promise.all resolution
private static class AsyncFromHandler extends BaseFunction {
private final Scriptable result;
private final Function mapFn;
private final Scriptable thisArg;
private final Function resolve;

AsyncFromHandler(Scriptable result, Function mapFn, Scriptable thisArg, Function resolve) {
this.result = result;
this.mapFn = mapFn;
this.thisArg = thisArg;
this.resolve = resolve;
}

@Override
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
if (args.length > 0 && args[0] instanceof Scriptable) {
Scriptable values = (Scriptable) args[0];
long len = getLengthProperty(cx, values);

for (long k = 0; k < len; k++) {
Object value = getElem(cx, values, k);
if (mapFn != null) {
value =
mapFn.call(
cx, scope, thisArg, new Object[] {value, Long.valueOf(k)});
}
ArrayLikeAbstractOperations.defineElem(cx, result, k, value);
}
setLengthProperty(cx, result, len);
}
resolve.call(cx, scope, null, new Object[] {result});
return Undefined.instance;
}

@Override
public Scriptable construct(Context cx, Scriptable scope, Object[] args) {
throw ScriptRuntime.typeErrorById("msg.not.ctor", "AsyncFromHandler");
}
}

private static Object js_of(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
final Scriptable result =
callConstructorOrCreateArray(cx, scope, thisObj, args.length, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,9 @@ msg.no.new =\
msg.map.function.not =\
Map function is not actually a function

msg.no.promise =\
Promise is not available

msg.constructor.no.function =\
The constructor for {0} may not be invoked as a function

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.mozilla.javascript.tests.es2024;

import org.junit.Test;
import org.mozilla.javascript.testutils.Utils;

/**
* Tests for ES2024 Array.fromAsync
*
* <p>Note: Full async testing requires manual promise queue processing in Rhino. These tests verify
* basic functionality.
*/
public class ArrayFromAsyncTest {

@Test
public void testArrayFromAsyncExists() {
String script = "typeof Array.fromAsync === 'function';";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncReturnsPromise() {
String script = "Array.fromAsync([1, 2, 3]) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncWithEmptyArray() {
String script = "Array.fromAsync([]) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncWithPromiseArray() {
String script =
"Array.fromAsync([Promise.resolve(1), Promise.resolve(2)]) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncWithIterable() {
String script =
"var iterable = {"
+ " [Symbol.iterator]: function* () { yield 1; yield 2; }"
+ "};"
+ "Array.fromAsync(iterable) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncWithArrayLike() {
String script =
"var arrayLike = { 0: 'a', 1: 'b', length: 2 };"
+ "Array.fromAsync(arrayLike) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncWithMapper() {
String script =
"Array.fromAsync([1, 2], function(x) { return x * 2; }) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncWithMapperAndThis() {
String script =
"var obj = { multiplier: 2 };"
+ "Array.fromAsync([1, 2], function(x) { return x * this.multiplier; }, obj) instanceof Promise;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncLength() {
String script = "Array.fromAsync.length === 1;";
Utils.assertWithAllModes_ES6(true, script);
}

@Test
public void testArrayFromAsyncName() {
String script = "Array.fromAsync.name === 'fromAsync';";
Utils.assertWithAllModes_ES6(true, script);
}
}
Loading