Skip to content

Commit

Permalink
Support ES2019 Array.prototype.flatMap (#1372)
Browse files Browse the repository at this point in the history
* feat: implement Array.prototype.flatMap
* tests: update test262.properties
  • Loading branch information
midgleyc committed Aug 18, 2023
1 parent 04a2e13 commit 97c51f5
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 21 deletions.
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

0 comments on commit 97c51f5

Please sign in to comment.