-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 array.prototype.flat and flatMap #5573
Conversation
// b. Let exists be ? HasProperty(source, P). | ||
if (sourceIndex in source) { | ||
// c. If exists is true, then | ||
// i. Let element be ? Get(source, P). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the code as written work correctly in the presence of proxies / getters? The spec seems to say that the element is accessed once, while below you access it multiple times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point - that's not in the test262 tests and I didn't think of it - currently this will certainly not work correctly with proxies/getters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should now be fixed - I've also added a case using a Proxy to test this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’d be great to contribute those tests back to test262 :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ljharb I'll endeavour to contribute some equivalent tests to test262 later this week - I've not written a test262 test before so will take me a bit of time to understand how to - I can see immediately though that I can't simply submit the same there as I have here as the test262 harness is very different to CC's UTF.
// 1. Let elementLen be ? ToLength(? Get(element, "length")). | ||
// 2. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, depth - 1). | ||
if (__chakraLibrary.isArray(element)) { | ||
targetIndex = __chakraLibrary.FlattenIntoArray(target, element, element.length |0, targetIndex, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you set the depth in the recursive call to 0 rather than depth -1
, is that intentional? I also saw that you don't check for depth > 0
here either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you don't have a depth at all... Should this be recursively calling into FlattenIntoArrayMapped
and passing the mapper function down as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional - I should probably comment it. flatMap always uses depth = 1 and this point is only reached from flatMap (and 1 -1 = 0).
The deviation from spec text is because I split the spec's FlattenIntoArray function into 2 separate functions one for where a callback function is needed and one for where it's not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, just read the spec. I didn't expect that from my intuition about these functions, but I agree with you. While adding the comment, I'd also appreciate one stating that the thisArg
of the spec is expected to be bound to the mapperFunction
by the caller.
Regarding testing the length-too-long error; I'd be surprised if any engine today can hit that case... |
Regarding the failure with passing @zenparsing do you know if binding |
Oh; null and undefined aren't replaced if the function is a strict function. I see now that the test case says that it should only be run in strict mode, how were you running it @rhuanjl ? Were you using the test 262 test harness, or just running that one file? |
I made my own mini-harness to run the test files. That case still failed with strict mode though - without strict mode most of the cases in the file failed, with strict mode only that case (the one with null) failed. |
// 1. Let elementLen be ? ToLength(? Get(element, "length")). | ||
// 2. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, depth - 1). | ||
element = source[sourceIndex]; | ||
targetIndex = __chakraLibrary.FlattenIntoArray(target, element, element.length |0, targetIndex, depth - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this operation (element.length |0
) is not doing the same thing as ToLength. Seems the operation is closer to ToInteger since it does not clamp negative values to 0 like ToLength does.
// iii. Let shouldFlatten be false. | ||
// iv. If depth > 0, then | ||
// 1. Set shouldFlatten to ? IsArray(element). | ||
if (depth > 0 && __chakraLibrary.isArray(source[sourceIndex])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, we should not be doing this property get for source[sourceIndex]
here as well as another lookup in the conditional branches.
//5. Let A be ? ArraySpeciesCreate(O, 0). | ||
const A = __chakraLibrary.arraySpeciesCreate(o, 0); | ||
//6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum). | ||
if (!thisArg) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the reason the test is failing; you are just checking the truthiness. Undefined works because when you call a strict function foo()
it will get undefined
as its this, but with this check passing false or 0 or the empty string or any other falsy value would fail I'd suggest changing this to thisArg === undefined
, and in all other cases going with the bind in the else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
targetIndex = __chakraLibrary.FlattenIntoArray(target, element, element.length |0, targetIndex, 0); | ||
} else { | ||
// vi. Else, | ||
// 1. If targetIndex >= 253-1, throw a TypeError exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Little typo here, should probably be 2^53-1 not 253-1.
Apart from those small comments, this looks reasonable to me. Thanks for taking this on! I'm not sure why the no_icu_static_ubuntu_linux_debug test is failing, it looks like somehow the two functions haven't been added to the array prototype, but in the normal static_ubuntu_linux_debug CI run the test did run and pass. |
Ah, and just saw that windows 10 ci_lite_x64_debug also failed in the same way. |
Do the windows lite build and ubuntu no_icu builds build without JsBuiltins? (these functions are the first JsBuiltins without a fall back C++ implementation so if JsBuiltins are disabled these won't work) |
Looks like that might be the case: https://github.com/Microsoft/ChakraCore/blob/0fe9eb90a69fa4d148013241276d1824676f628e/lib/Common/CommonDefines.h#L133-L136 lite or non-intl-non-windows builds don't enable js builtins it seems. It looks like we don't have a specific flag to avoid just lite runs, but you could add the Intl tag to your test rlexe.xml file for now, I think that will limit it to appropriate flavors |
Thank you for the reviews @MSLaguana @boingoing I think I've fixed all the points you've raised - I'll hold off on squashing for now as I anticipate that some further reviews/changes will be required. I'm nervous about non-icu/lite builds not supporting this - could be confusing for some users, should/could we either: |
Just spoke with @curtisman about this, he recommends option 1, although we need to investigate whether that will require another flavor of checked in generated bytecode if the lite build of chakracore is sufficiently different to the nojit flavor. |
Lite build seems to work with the "normal" no-jit bytecode - tested offline and CI runs with it - as can be seen. I figure that a change of that kind should perhaps be a separate PR though? (I added it here just to test the idea) |
#endif | ||
|
||
#define ENABLE_JS_BUILTINS // Built In functions support |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sigatrev @meg-gupta If we enable JS Builtins for Lite builds, do you know if there is any reason aside from historical reference to keep native implementations of JS Builtins around?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume it's historical, so that we had a way to back out if needed. Js Builtins are relatively new with the first one, indexOf, it was not immediately clear if the benefits were significant.
sourceLen = __chakraLibrary.GetLength(o); | ||
} | ||
//3. If IsCallable(mapperFunction) is false throw a TypeError exception | ||
if (!__chakraLibrary.isCallable(mapperFunction)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When implementing Array.prototype.filter we concluded that if (typeof callbackfn != "function")
was sufficient to check IsCallable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that, per spec, typeof x === 'function'
is exactly equivalent to IsCallable(x)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For an engine, this is correct. In libraries that need to account for older engines where this does not hold, they'd need a more intensive check - but chakra shouldn't need to worry about that.
Thanks for taking the time to implement this. I took a lot at the JIT output and it seems to be doing a reasonable job, but recussions isn't handled very precisely when it comes to inlining. It'd be interesting to see how perf would compare to an implementation using a stack instead of recursion, but that's not strictly necessary for this PR. |
@sigatrev Thanks for the review. I've:
|
@rhuanjl I did some microbenchmarking and the original recursive case runs ~30% faster. Thanks for giving that a shot, and the quick turn-around. |
OK, I've updated to remove the stack based mechanism, I've kept the inner loop for FlattenIntoArrayMapped though as I assume that has minimal performance impact and if anything makes the code clearer. |
sourceLen = __chakraLibrary.GetLength(o); | ||
} | ||
//3. If IsCallable(mapperFunction) is false throw a TypeError exception | ||
if (typeof mapperFunction != "function") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: !==
is generally preferable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied that from Array.prototype.filter above - so if changing this one will change filter as well for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking our feedback. Looks good to me.
// depth = 1 and the presence of a mapperFunction guaranteed | ||
// both these conditions are always met when this is called from flatMap | ||
// Additionally this is slightly refactored rather than taking a thisArg | ||
// the calling funciton binds the thisArg if it's required |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: spelling in these comment lines (funciton
-> function
and FlatternIntoArray
-> FlattenIntoArray
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
while (innerIndex < innerLength) { | ||
if (innerIndex in element) { | ||
// 1. If targetIndex >= 2^53-1, throw a TypeError exception. | ||
if (targetIndex >= 9007199254740991 /* 2^53 -1 */) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove space before -1
for consistency
} else { | ||
// vi. Else, | ||
// 1. If targetIndex >= 2^53-1, throw a TypeError exception. | ||
if (targetIndex >= 9007199254740991 /* 2^53 -1 */) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove space before -1
for consistency
@dotnet-bot test OSX static_osx_osx_debug please |
Please let me know what's needed for this to be merged. I assume it will require a GUID update as it changes the byte code, anything else? |
@rhuanjl I think this is pretty much ready. I should be able to get it merged in the next couple of days, sorry for the delay. |
@rhuanjl can you enable us to update the PR so I update bytecode headers and GUID? |
@sigatrev Allow edits from maintainers is already ticked not sure if there's anything else I can do to give you additional access? (let me know if you'd like me to regen the byte code) |
Merge pull request #5573 from rhuanjl:flat This PR implements the stage 3 proposal Array methods: Array.prototype.flat and Array.prototype.flatMap as JsBuiltIns. Draft specification text is here: https://tc39.github.io/proposal-flatMap/ Notes: 1. 3 test262 failures: - length is not configurable (for both methods), this is due to ScriptFunction lengths in CC all not being configurable and should be fixed if #5405 is merged - the case using null as this fails -- EDIT: FIXED 2. I can't find a feasible way to test the length is too big error - not sure if CC can actually handle arrays that big anyway so maybe not worth implementing that case? 3. Should probably have a few more CI tests Fixes #5543
Thanks for testing and merging this @sigatrev And thanks everyone for the reviews. |
@rhuanjl Thanks for your patience. We really appreciate the contributions. |
This PR implements the stage 3 proposal Array methods: Array.prototype.flat and Array.prototype.flatMap as JsBuiltIns.
Draft specification text is here: https://tc39.github.io/proposal-flatMap/
Notes:
Fixes #5543