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

Support ES2019 Array.prototype.flatMap #1372

Merged
merged 4 commits into from
Aug 18, 2023
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
92 changes: 75 additions & 17 deletions src/org/mozilla/javascript/NativeArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ protected void initPrototypeId(int id) {
arity = 0;
s = "flat";
break;
case Id_flatMap:
arity = 1;
s = "flatMap";
break;
default:
throw new IllegalArgumentException(String.valueOf(id));
}
Expand Down Expand Up @@ -441,6 +445,9 @@ public Object execIdCall(
case Id_flat:
return js_flat(cx, scope, thisObj, args);

case Id_flatMap:
return js_flatMap(cx, scope, thisObj, args);

case Id_every:
case Id_filter:
case Id_forEach:
Expand Down Expand Up @@ -1133,6 +1140,27 @@ private static String toStringHelper(
return result.toString();
}

private static Function getCallbackArg(Context cx, Object callbackArg) {
if (!(callbackArg instanceof Function)) {
throw ScriptRuntime.notFunctionError(callbackArg);
}
if (cx.getLanguageVersion() >= Context.VERSION_ES6
&& (callbackArg instanceof NativeRegExp)) {
// Previously, it was allowed to pass RegExp instance as a callback (it implements
// Function)
// But according to ES2015 21.2.6 Properties of RegExp Instances:
// > RegExp instances are ordinary objects that inherit properties from the RegExp
// prototype object.
// > RegExp instances have internal slots [[RegExpMatcher]], [[OriginalSource]], and
// [[OriginalFlags]].
// so, no [[Call]] for RegExp-s
throw ScriptRuntime.notFunctionError(callbackArg);
}

Function f = (Function) callbackArg;
return f;
}

/** See ECMA 15.4.4.3 */
private static String js_join(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
Expand Down Expand Up @@ -2038,6 +2066,47 @@ private static Scriptable flat(Context cx, Scriptable scope, Scriptable source,
return result;
}

private static Object js_flatMap(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
Object callbackArg = args.length > 0 ? args[0] : Undefined.instance;

Function f = getCallbackArg(cx, callbackArg);
Scriptable parent = ScriptableObject.getTopLevelScope(f);
Scriptable thisArg;
if (args.length < 2 || args[1] == null || args[1] == Undefined.instance) {
thisArg = parent;
} else {
thisArg = ScriptRuntime.toObject(cx, scope, args[1]);
}

long length = getLengthProperty(cx, o);

Scriptable result;
result = cx.newArray(scope, 0);
long j = 0;
for (long i = 0; i < length; i++) {
Object elem = getRawElem(o, i);
if (elem == Scriptable.NOT_FOUND) {
continue;
}
Object[] innerArgs = new Object[] {elem, Long.valueOf(i), o};
Object mapCall = f.call(cx, parent, thisArg, innerArgs);
if (js_isArray(mapCall)) {
Scriptable arr = (Scriptable) mapCall;
long arrLength = getLengthProperty(cx, arr);
for (long k = 0; k < arrLength; k++) {
Object temp = getRawElem(arr, k);
defineElemOrThrow(cx, result, j++, temp);
}
} else {
defineElemOrThrow(cx, result, j++, mapCall);
}
}
setLengthProperty(cx, result, j);
return result;
}

/** Implements the methods "every", "filter", "forEach", "map", and "some". */
private static Object iterativeMethod(
Context cx,
Expand All @@ -2063,23 +2132,8 @@ private static Object iterativeMethod(
}

Object callbackArg = args.length > 0 ? args[0] : Undefined.instance;
if (callbackArg == null || !(callbackArg instanceof Function)) {
throw ScriptRuntime.notFunctionError(callbackArg);
}
if (cx.getLanguageVersion() >= Context.VERSION_ES6
&& (callbackArg instanceof NativeRegExp)) {
// Previously, it was allowed to pass RegExp instance as a callback (it implements
// Function)
// But according to ES2015 21.2.6 Properties of RegExp Instances:
// > RegExp instances are ordinary objects that inherit properties from the RegExp
// prototype object.
// > RegExp instances have internal slots [[RegExpMatcher]], [[OriginalSource]], and
// [[OriginalFlags]].
// so, no [[Call]] for RegExp-s
throw ScriptRuntime.notFunctionError(callbackArg);
}

Function f = (Function) callbackArg;
Function f = getCallbackArg(cx, callbackArg);
Scriptable parent = ScriptableObject.getTopLevelScope(f);
Scriptable thisArg;
if (args.length < 2 || args[1] == null || args[1] == Undefined.instance) {
Expand Down Expand Up @@ -2623,6 +2677,9 @@ protected int findPrototypeId(String s) {
case "flat":
id = Id_flat;
break;
case "flatMap":
id = Id_flatMap;
break;
default:
id = 0;
break;
Expand Down Expand Up @@ -2663,7 +2720,8 @@ protected int findPrototypeId(String s) {
Id_copyWithin = 31,
Id_at = 32,
Id_flat = 33,
SymbolId_iterator = 34,
Id_flatMap = 34,
SymbolId_iterator = 35,
MAX_PROTOTYPE_ID = SymbolId_iterator;
private static final int ConstructorId_join = -Id_join,
ConstructorId_reverse = -Id_reverse,
Expand Down
171 changes: 171 additions & 0 deletions testsrc/jstests/es2019/array-flat-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Taken from https://github.com/v8/v8/blob/main/test/mjsunit/harmony/array-flat-map.js and changed due to Rhino errors
// TypeError: redeclaration of const input

// Flags: --allow-natives-syntax
load("testsrc/assert.js");

let input;
let result;

assertEquals(Array.prototype.flatMap.length, 1);
assertEquals(Array.prototype.flatMap.name, 'flatMap');

assertEquals(
[1, 2, 3, 4].flatMap((element) => [element, element ** 2]),
[1, 1, 2, 4, 3, 9, 4, 16]
);
assertEquals(
[1, 2, 3, 4].flatMap((element) => [[element, element ** 2]]),
[[1, 1], [2, 4], [3, 9], [4, 16]]
);

{
const elements = new Set([
-Infinity,
-1,
-0,
+0,
+1,
Infinity,
null,
undefined,
true,
false,
'',
'foo',
/./,
[],
{},
Object.create(null),
//new Proxy({}, {}),
Symbol(),
x => x ** 2,
String
]);

for (let value of elements) {
assertEquals(
[value].flatMap((element) => [element, element]),
[value, value]
);
}
}

{
const array = [42];
assertEquals(
[array].flatMap((element) => [element, element]),
[array, array]
);
}

{
const nonCallables = new Set([
-Infinity,
-1,
-0,
+0,
+1,
Infinity,
null,
undefined,
true,
false,
'',
'foo',
/./,
[],
{},
Object.create(null),
//new Proxy({}, {}),
Symbol(),
]);
for (let nonCallable of nonCallables) {
assertThrows(() => {
[].flatMap(nonCallable);
}, TypeError);
}
}

{
const object = {
foo: 42,
get length() {
object.foo = 0;
}
};
result = [object].flatMap((element) => [element, element]);
//%HeapObjectVerify(result);
assertEquals(result, [object, object]);
assertEquals(result[0].foo, 42);
}

assertThrows(() => {
Array.prototype.flatMap.call(null, (element) => element);
}, TypeError);
assertThrows(() => {
Array.prototype.flatMap.call(undefined, (element) => element);
}, TypeError);

assertEquals(
Array.prototype.flatMap.call(
{
length: 1,
0: 'a',
1: 'b',
},
(element) => element
),
['a']
);
assertEquals(
Array.prototype.flatMap.call(
{
length: 2,
0: 'a',
1: 'b',
},
(element) => element
),
['a', 'b']
);

{
result = [1, 2, 3].flatMap(function() {
return [this];
}, 'abc');
assertEquals(true, result[0] == 'abc');
assertEquals(true, result[1] == 'abc');
assertEquals(true, result[2] == 'abc');
}

{
input = { 0: 'a', 1: 'b', 2: 'c', length: 'wat' };
assertEquals(Array.prototype.flatMap.call(input, x => [x]), []);
}

{
let count = 0;
input = {
get length() { ++count; return 0; }
};
result = Array.prototype.flatMap.call(input, x => [x]);
assertEquals(count, 1);
}

{
const descriptor = Object.getOwnPropertyDescriptor(
Array.prototype,
'flatMap'
);
assertEquals(descriptor.value, Array.prototype.flatMap);
assertEquals(descriptor.writable, true);
assertEquals(descriptor.enumerable, false);
assertEquals(descriptor.configurable, true);
}

"success";
1 change: 0 additions & 1 deletion testsrc/org/mozilla/javascript/tests/Test262SuiteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ public class Test262SuiteTest {
static final Set<String> UNSUPPORTED_FEATURES =
new HashSet<>(
Arrays.asList(
"Array.prototype.flatMap",
"Atomics",
"IsHTMLDDA",
"Proxy",
Expand Down
14 changes: 14 additions & 0 deletions testsrc/org/mozilla/javascript/tests/es2019/ArrayFlatMapTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript.tests.es2019;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.drivers.LanguageVersion;
import org.mozilla.javascript.drivers.RhinoTest;
import org.mozilla.javascript.drivers.ScriptTestsBase;

@RhinoTest("testsrc/jstests/es2019/array-flat-map.js")
@LanguageVersion(Context.VERSION_ES6)
public class ArrayFlatMapTest extends ScriptTestsBase {}
17 changes: 14 additions & 3 deletions testsrc/test262.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This is a configuration file for Test262SuiteTest.java. See ./README.md for more info about this file

built-ins/Array 188/2670 (7.04%)
built-ins/Array 179/2670 (6.7%)
from/calling-from-valid-1-noStrict.js non-strict Spec pretty clearly says this should be undefined
from/elements-deleted-after.js Checking to see if length changed, but spec says it should not
from/iter-map-fn-this-non-strict.js non-strict Error propagation needs work in general
Expand Down Expand Up @@ -72,7 +72,18 @@ built-ins/Array 188/2670 (7.04%)
prototype/filter/target-array-with-non-writable-property.js {unsupported: [Symbol.species]}
prototype/findIndex/predicate-call-this-strict.js strict
prototype/find/predicate-call-this-strict.js strict
prototype/flatMap 21/21 (100.0%)
prototype/flatMap/array-like-objects.js
prototype/flatMap/array-like-objects-poisoned-length.js
prototype/flatMap/proxy-access-count.js
prototype/flatMap/target-array-non-extensible.js {unsupported: [Symbol.species]}
prototype/flatMap/target-array-with-non-configurable-property.js {unsupported: [Symbol.species]}
prototype/flatMap/target-array-with-non-writable-property.js {unsupported: [Symbol.species]}
prototype/flatMap/this-value-ctor-non-object.js
prototype/flatMap/this-value-ctor-object-species.js {unsupported: [Symbol.species]}
prototype/flatMap/this-value-ctor-object-species-bad-throws.js {unsupported: [Symbol.species]}
prototype/flatMap/this-value-ctor-object-species-custom-ctor.js {unsupported: [Symbol.species]}
prototype/flatMap/this-value-ctor-object-species-custom-ctor-poisoned-throws.js {unsupported: [Symbol.species]}
prototype/flatMap/thisArg-argument.js strict
prototype/flat/non-object-ctor-throws.js
prototype/flat/proxy-access-count.js
prototype/flat/target-array-non-extensible.js {unsupported: [Symbol.species]}
Expand Down Expand Up @@ -159,7 +170,7 @@ built-ins/Array 188/2670 (7.04%)
prototype/toLocaleString/primitive_this_value.js strict
prototype/toLocaleString/primitive_this_value_getter.js strict
prototype/unshift/throws-with-string-receiver.js
prototype/methods-called-as-functions.js {unsupported: [Symbol.species, Array.prototype.flatMap]}
prototype/methods-called-as-functions.js {unsupported: [Symbol.species]}
prototype/Symbol.iterator.js Expects a particular string value
Symbol.species 4/4 (100.0%)
proto-from-ctor-realm-one.js {unsupported: [Reflect]}
Expand Down
Loading