Skip to content

Commit 819ebeb

Browse files
rhuanjlsigatrev
authored andcommitted
array.prototype.flat AND array.prototype.flatMap
1 parent 4b8ec55 commit 819ebeb

11 files changed

+357
-24
lines changed

lib/Parser/rterrors.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ RT_ERROR_MSG(JSERROR_SetPrototypeOf, 5616, "Failed to set prototype", "Failed to
302302
RT_ERROR_MSG(JSERR_ObjectIsNotInitialized, 5617, "%s: Object internal state is not initialized", "Object internal state is not initialized", kjstTypeError, 0)
303303

304304
RT_ERROR_MSG(JSERR_GeneratorAlreadyExecuting, 5618, "%s: Cannot execute generator function because it is currently executing", "", kjstTypeError, 0)
305-
// 5619-5626 Unused
305+
RT_ERROR_MSG(JSERR_LengthIsTooBig, 5619, "Length property would exceed maximum value in output from '%s'", "", kjstTypeError, 0)
306+
// 5620-5626 Unused
306307
RT_ERROR_MSG(JSERR_NeedConstructor, 5627, "'%s' is not a constructor", "Constructor expected", kjstTypeError, 0)
307308

308309
RT_ERROR_MSG(VBSERR_CantDisplayDate, 32812, "", "The specified date is not available in the current locale's calendar", kjstRangeError, 0)

lib/Runtime/Base/JnDirectFields.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ ENTRY(every)
140140
ENTRY(exec)
141141
ENTRY2(false_, _u("false")) // "false" cannot be an identifier in C++ so using "false_" instead
142142
ENTRY(flags)
143+
ENTRY(flat)
144+
ENTRY(flatMap)
143145
ENTRY(fill)
144146
ENTRY(filter)
145147
ENTRY(finally)
@@ -657,6 +659,7 @@ ENTRY(builtInRegexMatch)
657659
ENTRY(builtInCallInstanceFunction)
658660
ENTRY(raiseInvalidCurrencyCode)
659661
ENTRY(raiseInvalidDate)
662+
ENTRY(raiseLengthIsTooBig)
660663
ENTRY(raiseLocaleNotWellFormed)
661664
ENTRY(raiseMissingCurrencyCode)
662665
ENTRY(raiseNeedObject)

lib/Runtime/Library/EngineInterfaceObjectBuiltIns.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ BuiltInRaiseException1(RangeError, LocaleNotWellFormed)
5858
BuiltInRaiseException1(TypeError, This_NullOrUndefined)
5959
BuiltInRaiseException1(TypeError, NotAConstructor)
6060
BuiltInRaiseException1(TypeError, ObjectIsNonExtensible)
61+
BuiltInRaiseException1(TypeError, LengthIsTooBig)
6162
BuiltInRaiseException2(TypeError, NeedObjectOfType)
6263
BuiltInRaiseException1(RangeError, InvalidCurrencyCode)
6364
BuiltInRaiseException(TypeError, MissingCurrencyCode)

lib/Runtime/Library/JavascriptLibrary.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1669,7 +1669,7 @@ namespace Js
16691669

16701670
bool JavascriptLibrary::InitializeArrayPrototype(DynamicObject* arrayPrototype, DeferredTypeHandlerBase * typeHandler, DeferredInitializeMode mode)
16711671
{
1672-
typeHandler->Convert(arrayPrototype, mode, 24);
1672+
typeHandler->Convert(arrayPrototype, mode, 26);
16731673
// Note: Any new function addition/deletion/modification should also be updated in JavascriptLibrary::ProfilerRegisterArray
16741674
// so that the update is in sync with profiler
16751675

lib/Runtime/Library/JsBuiltIn/JsBuiltIn.js

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
ArrayEntries: { className: "Array", methodName: "entries", argumentsCount: 0, forceInline: true /*optional*/ },
1515
ArrayIndexOf: { className: "Array", methodName: "indexOf", argumentsCount: 1, forceInline: true /*optional*/ },
1616
ArrayFilter: { className: "Array", methodName: "filter", argumentsCount: 1, forceInline: true /*optional*/ },
17+
ArrayFlat: { className: "Array", methodName: "flat", argumentsCount: 0, forceInline: true /*optional*/ },
18+
ArrayFlatMap: { className: "Array", methodName: "flatMap", argumentsCount: 1, forceInline: true /*optional*/ },
1719
};
1820

1921
var setPrototype = platform.builtInSetPrototype;
@@ -36,6 +38,7 @@
3638
__chakraLibrary.ArrayIterator.prototype = CreateObject(iteratorPrototype);
3739
__chakraLibrary.raiseNeedObjectOfType = platform.raiseNeedObjectOfType;
3840
__chakraLibrary.raiseThis_NullOrUndefined = platform.raiseThis_NullOrUndefined;
41+
__chakraLibrary.raiseLengthIsTooBig = platform.raiseLengthIsTooBig;
3942
__chakraLibrary.raiseFunctionArgument_NeedFunction = platform.raiseFunctionArgument_NeedFunction;
4043
__chakraLibrary.callInstanceFunc = platform.builtInCallInstanceFunction;
4144
__chakraLibrary.functionBind = platform.builtInJavascriptFunctionEntryBind;
@@ -200,7 +203,7 @@
200203
len = __chakraLibrary.GetLength(o);
201204
}
202205

203-
if (typeof callbackfn != "function") {
206+
if (typeof callbackfn !== "function") {
204207
__chakraLibrary.raiseFunctionArgument_NeedFunction("Array.prototype.filter");
205208
}
206209

@@ -238,5 +241,178 @@
238241

239242
return a;
240243
});
241-
244+
245+
platform.registerChakraLibraryFunction("FlattenIntoArray", function(target, source, sourceLen, start, depth)
246+
{
247+
// this is FlattenIntoArray from the flat/flatMap proposal BUT with no mapperFunction
248+
// a seperate function has been made to handle the case where there is a mapperFunction
249+
"use strict";
250+
//1. Let targetIndex be start.
251+
let targetIndex = start;
252+
//2. Let sourceIndex be 0.
253+
let sourceIndex = 0;
254+
//3. Repeat, while sourceIndex < sourceLen
255+
let element;
256+
while (sourceIndex < sourceLen) {
257+
// a. Let P be ! ToString(sourceIndex).
258+
// b. Let exists be ? HasProperty(source, P).
259+
if (sourceIndex in source) {
260+
// c. If exists is true, then
261+
// i. Let element be ? Get(source, P).
262+
element = source[sourceIndex];
263+
// ii. If mapperFunction is present - skipped see separate function
264+
// iii. Let shouldFlatten be false.
265+
// iv. If depth > 0, then
266+
// 1. Set shouldFlatten to ? IsArray(element).
267+
if (depth > 0 && __chakraLibrary.isArray(element)) {
268+
// v. If shouldFlatten is true, then
269+
// 1. Let elementLen be ? ToLength(? Get(element, "length")).
270+
// 2. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, depth - 1).
271+
targetIndex = __chakraLibrary.FlattenIntoArray(target, element, __chakraLibrary.toLength(element.length), targetIndex, depth - 1);
272+
} else {
273+
// vi. Else,
274+
// 1. If targetIndex >= 2^53-1, throw a TypeError exception.
275+
if (targetIndex >= 9007199254740991 /* 2^53-1 */) {
276+
__chakraLibrary.raiseLengthIsTooBig("Array.prototype.flat");
277+
}
278+
// 2. Perform ? CreateDataPropertyOrThrow(target, ! ToString(targetIndex), element).
279+
__chakraLibrary.arrayCreateDataPropertyOrThrow(target, targetIndex, element);
280+
// 3. Increase targetIndex by 1.
281+
++targetIndex;
282+
}
283+
}
284+
// d. Increase sourceIndex by 1.
285+
++sourceIndex;
286+
}
287+
//4. Return targetIndex.
288+
return targetIndex;
289+
});
290+
291+
platform.registerChakraLibraryFunction("FlattenIntoArrayMapped", function(target, source, sourceLen, start, mapperFunction) {
292+
"use strict";
293+
// this is FlattenIntoArray from the flat/flatMap proposal BUT with:
294+
// depth = 1 and the presence of a mapperFunction guaranteed
295+
// both these conditions are always met when this is called from flatMap
296+
// Additionally this is slightly refactored rather than taking a thisArg
297+
// the calling function binds the thisArg if it's required
298+
//1. Let targetIndex be start.
299+
let targetIndex = start;
300+
//2. Let sourceIndex be 0.
301+
let sourceIndex = 0;
302+
//3. Repeat, while sourceIndex < sourceLen
303+
304+
let element, innerLength, innerIndex;
305+
while (sourceIndex < sourceLen) {
306+
// a. Let P be ! ToString(sourceIndex).
307+
// b. Let exists be ? HasProperty(source, P).
308+
if (sourceIndex in source) {
309+
// c. If exists is true, then
310+
// i. Let element be ? Get(source, P).
311+
// ii. If mapperFunction is present, then
312+
// 1. Assert: thisArg is present.
313+
// 2. Set element to ? Call(mapperFunction, thisArg , element, sourceIndex, source).
314+
element = mapperFunction(source[sourceIndex], sourceIndex, source);
315+
// iii. Let shouldFlatten be false.
316+
// iv. If depth > 0, then
317+
// 1. Set shouldFlatten to ? IsArray(element).
318+
// v. If shouldFlatten is true, then
319+
// 1. Let elementLen be ? ToLength(? Get(element, "length")).
320+
// 2. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, depth - 1).
321+
if (__chakraLibrary.isArray(element)) {
322+
// instead of calling FlattenIntoArray use a simple loop here - as depth is always 0
323+
innerLength = __chakraLibrary.toLength(element.length);
324+
innerIndex = 0;
325+
while (innerIndex < innerLength) {
326+
if (innerIndex in element) {
327+
// 1. If targetIndex >= 2^53-1, throw a TypeError exception.
328+
if (targetIndex >= 9007199254740991 /* 2^53-1 */) {
329+
__chakraLibrary.raiseLengthIsTooBig("Array.prototype.flatMap");
330+
}
331+
// 2. Perform ? CreateDataPropertyOrThrow(target, ! ToString(targetIndex), element).
332+
__chakraLibrary.arrayCreateDataPropertyOrThrow(target, targetIndex, element[innerIndex]);
333+
// 3. Increase targetIndex by 1.
334+
++targetIndex;
335+
}
336+
++innerIndex;
337+
}
338+
} else {
339+
// vi. Else,
340+
// 1. If targetIndex >= 2^53-1, throw a TypeError exception.
341+
if (targetIndex >= 9007199254740991 /* 2^53-1 */) {
342+
__chakraLibrary.raiseLengthIsTooBig("Array.prototype.flatMap");
343+
}
344+
// 2. Perform ? CreateDataPropertyOrThrow(target, ! ToString(targetIndex), element).
345+
__chakraLibrary.arrayCreateDataPropertyOrThrow(target, targetIndex, element);
346+
// 3. Increase targetIndex by 1.
347+
++targetIndex;
348+
}
349+
}
350+
// d. Increase sourceIndex by 1.
351+
++sourceIndex;
352+
}
353+
//4. Return targetIndex.
354+
return targetIndex;
355+
});
356+
357+
platform.registerFunction(FunctionsEnum.ArrayFlat, function (depth) {
358+
"use strict";
359+
//1. Let O be ? ToObject(this value).
360+
//2. Let sourceLen be ? ToLength(? Get(O, "length")).
361+
let o, sourceLen;
362+
363+
if (__chakraLibrary.isArray(this)) {
364+
o = this;
365+
sourceLen = o.length;
366+
} else {
367+
if (this === null || this === undefined) {
368+
__chakraLibrary.raiseThis_NullOrUndefined("Array.prototype.flat");
369+
}
370+
o = __chakraLibrary.Object(this);
371+
sourceLen = __chakraLibrary.GetLength(o);
372+
}
373+
//3. Let depthNum be 1.
374+
//4. If depth is not undefined, then
375+
//5. Set depthNum to ? ToInteger(depth).
376+
const depthNum = depth !== undefined ? __chakraLibrary.toInteger(depth) : 1;
377+
//6. Let A be ? ArraySpeciesCreate(O, 0).
378+
const A = __chakraLibrary.arraySpeciesCreate(o, 0);
379+
//7. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum).
380+
__chakraLibrary.FlattenIntoArray(A, o, sourceLen, 0, depthNum);
381+
//8. Return A.
382+
return A;
383+
});
384+
385+
platform.registerFunction(FunctionsEnum.ArrayFlatMap, function (mapperFunction, thisArg) {
386+
"use strict";
387+
//1. Let O be ? ToObject(this value).
388+
//2. Let sourceLen be ? ToLength(? Get(O, "length")).
389+
let o, sourceLen;
390+
if (__chakraLibrary.isArray(this)) {
391+
o = this;
392+
sourceLen = o.length;
393+
} else {
394+
if (this === null || this === undefined) {
395+
__chakraLibrary.raiseThis_NullOrUndefined("Array.prototype.flatMap");
396+
}
397+
o = __chakraLibrary.Object(this);
398+
sourceLen = __chakraLibrary.GetLength(o);
399+
}
400+
//3. If IsCallable(mapperFunction) is false throw a TypeError exception
401+
if (typeof mapperFunction !== "function") {
402+
__chakraLibrary.raiseFunctionArgument_NeedFunction("Array.prototype.flatMap");
403+
}
404+
//4. If thisArg is present, let T be thisArg; else let T be undefined
405+
//5. Let A be ? ArraySpeciesCreate(O, 0).
406+
const A = __chakraLibrary.arraySpeciesCreate(o, 0);
407+
//6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum).
408+
if (thisArg === undefined) {
409+
__chakraLibrary.FlattenIntoArrayMapped(A, o, sourceLen, 0, mapperFunction);
410+
} else {
411+
const func = __chakraLibrary.callInstanceFunc(__chakraLibrary.functionBind, mapperFunction, thisArg);
412+
__chakraLibrary.FlattenIntoArrayMapped(A, o, sourceLen, 0, func);
413+
}
414+
//7. Return A.
415+
return A;
416+
});
417+
242418
});

test/Array/array_flat.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//-------------------------------------------------------------------------------------------------------
2+
// Copyright (C) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
4+
//-------------------------------------------------------------------------------------------------------
5+
6+
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
7+
8+
function testFlat(input, output, depth)
9+
{
10+
assert.areEqual(output, Array.prototype.flat.call(input, depth));
11+
}
12+
13+
function testFlatMap(input, output, mappingFunction, thisArg)
14+
{
15+
assert.areEqual(output, Array.prototype.flatMap.call(input, mappingFunction, thisArg));
16+
}
17+
18+
const tests = [
19+
{
20+
name : "properties",
21+
body : function ()
22+
{
23+
assert.areEqual("flat", Array.prototype.flat.name, "Array.prototype.flat name should be flat");
24+
assert.areEqual("flatMap", Array.prototype.flatMap.name, "Array.prototype.flatMap name should be flatMap");
25+
assert.areEqual(0, Array.prototype.flat.length, "Array.prototype.flat length should be 0");
26+
assert.areEqual(1, Array.prototype.flatMap.length, "Array.prototype.flatMap length should be 1");
27+
}
28+
},
29+
{
30+
name : "flatten arrays",
31+
body : function ()
32+
{
33+
testFlat([2, 3, [4, 5]], [2, 3, 4, 5]);
34+
testFlat([2, 3, [4, [5, 6]]], [2, 3, 4, [5, 6]]);
35+
testFlat([2, 3, [4, [5, 6]]], [2, 3, 4, 5, 6], 2);
36+
testFlat([], []);
37+
testFlat([[], [], 1], [1]);
38+
const typedArr = new Int32Array(3);
39+
const typedArr2 = new Int32Array(3);
40+
typedArr[0] = 5;
41+
typedArr[1] = 6;
42+
typedArr[2] = 3;
43+
typedArr2[0] = 5;
44+
typedArr2[1] = 6;
45+
typedArr2[2] = 3;
46+
testFlat(typedArr, typedArr2);
47+
}
48+
},
49+
{
50+
name : "flatMap arrays",
51+
body : function ()
52+
{
53+
testFlatMap([2, 3, 4, 5], [2, 4, 3, 6, 4, 8, 5, 10], function (a) { return [a, a * 2]});
54+
const thisArg = { count : 0 };
55+
testFlatMap([2, 3, 4], [2, 3, 3, 4, 4, 5], function (a) { this.count += a; return [ a, a + 1]}, thisArg);
56+
testFlatMap([2, 3, 4], [[2], [3], [4]], function (a) { return [[a]]});
57+
assert.areEqual(9, thisArg.count);
58+
59+
assert.throws(()=>{[2, 3].flatMap("Not Callable")});
60+
assert.throws(()=>{[2, 3].flatMap(class NotCallable {})});
61+
}
62+
},
63+
{
64+
name : "flatMap abnormal this",
65+
body : function ()
66+
{
67+
"use strict";
68+
testFlatMap([2, 3], [null, null], function () { return [this]}, null);
69+
testFlatMap([2, 3], [undefined, undefined], function () { return [this]}, undefined);
70+
testFlatMap([2, 3], [undefined, undefined], function () { return [this]});
71+
testFlatMap([2, 3], ["", ""], function () { return [this]}, "");
72+
testFlatMap([2, 3], ["Test", "Test"], function () { return [this]}, "Test");
73+
const boo = {};
74+
testFlatMap([2, 3], [boo, boo], function () { return [this]}, boo);
75+
}
76+
},
77+
{
78+
name : "Proxied Array",
79+
body : function ()
80+
{
81+
let getCount = 0, hasCount = 0;
82+
const handler = {
83+
get : function (t, p, r) { ++getCount; return Reflect.get(t, p, r); },
84+
has : function (t, p, r) { ++hasCount; return Reflect.has(t, p, r); }
85+
}
86+
const prox = new Proxy ([2, [3, 5]], handler);
87+
testFlat(prox, [2, 3, 5]);
88+
assert.areEqual(4, getCount); // length and constructor are also accessed hence count 2 higher than length
89+
assert.areEqual(2, hasCount);
90+
const prox2 = new Proxy ([2, 3, 5], handler);
91+
testFlatMap(prox2, [2, 4, 3, 6, 5, 10], function (a) { return [a, a * 2]});
92+
assert.areEqual(9, getCount); // length and constructor are also accessed hence count 2 higher than length
93+
assert.areEqual(5, hasCount);
94+
}
95+
},
96+
{
97+
name : "Invalid object",
98+
body : function ()
99+
{
100+
assert.throws(() => {Array.prototype.flat.call(null)}, TypeError);
101+
assert.throws(() => {Array.prototype.flat.call(undefined)}, TypeError);
102+
assert.throws(() => {Array.prototype.flatMap.call(null)}, TypeError);
103+
assert.throws(() => {Array.prototype.flatMap.call(undefined)}, TypeError);
104+
}
105+
}
106+
];
107+
108+
testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });

test/Array/rlexe.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
<baseline>array_fastinit.baseline</baseline>
77
</default>
88
</test>
9+
<test>
10+
<default>
11+
<files>array_flat.js</files>
12+
<compile-flags>-args summary -endargs</compile-flags>
13+
<!-- Lite builds don't enable JsBuiltIns hence the array flat methods won't work on them - Intl flag identifies these builds-->
14+
<tags>Intl</tags>
15+
</default>
16+
</test>
917
<test>
1018
<default>
1119
<files>array_qsortr.js</files>

0 commit comments

Comments
 (0)