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

Implement array.prototype.flat and flatMap #5573

Merged
merged 3 commits into from
Aug 17, 2018
Merged

Conversation

rhuanjl
Copy link
Collaborator

@rhuanjl rhuanjl commented Aug 4, 2018

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:
  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

// b. Let exists be ? HasProperty(source, P).
if (sourceIndex in source) {
// c. If exists is true, then
// i. Let element be ? Get(source, P).
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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 :-)

Copy link
Collaborator Author

@rhuanjl rhuanjl Aug 16, 2018

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);
Copy link
Contributor

@MSLaguana MSLaguana Aug 5, 2018

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

@MSLaguana
Copy link
Contributor

Regarding testing the length-too-long error; I'd be surprised if any engine today can hit that case... 2^53-1 is a lot of items! If each just took up 1 bit, you'd still need ~2^50 bytes of memory... I guess if you were dedicated enough and willing to use swap space then you could get there... But for now I think we're ok without a test for that specific case.

@MSLaguana
Copy link
Contributor

Regarding the failure with passing null as thisArg, looks like that might be due to a spec change; see the note at https://tc39.github.io/ecma262/#sec-function.prototype.call

@zenparsing do you know if binding null or undefined is supposed to keep that value as-is in the called function, rather than substituting the global object?

@MSLaguana
Copy link
Contributor

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?

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 5, 2018

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);
Copy link
Contributor

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])) {
Copy link
Contributor

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) {
Copy link
Contributor

@MSLaguana MSLaguana Aug 5, 2018

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.

Copy link
Collaborator Author

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.
Copy link
Contributor

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.

@MSLaguana
Copy link
Contributor

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.

@MSLaguana
Copy link
Contributor

Ah, and just saw that windows 10 ci_lite_x64_debug also failed in the same way.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 5, 2018

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)

@MSLaguana
Copy link
Contributor

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

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 5, 2018

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:
a) enable JsBuiltIns for non-icu/lite builds - they don't need icu etc. OR
b) put in a stub that throws a message saying that these methods aren't supported in a non-icu/lite build. OR
c) Make a c++ implentation

@MSLaguana
Copy link
Contributor

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.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 6, 2018

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
Copy link
Contributor

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?

Copy link
Contributor

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)) {
Copy link
Contributor

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.

Copy link
Contributor

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).

Copy link
Collaborator

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.

@sigatrev
Copy link
Contributor

sigatrev commented Aug 6, 2018

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.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 7, 2018

@sigatrev Thanks for the review. I've:

  1. removed the IsCallable function AND
  2. as a separate commit I've refactored FlattenIntoArray to use a stack - kept as a separate commit so can be easily dropped if not wanted AND
  3. I've added an extra loop to FlattenIntoArrayMapped so it no longer has to call FlattenIntoArray

@sigatrev
Copy link
Contributor

sigatrev commented Aug 7, 2018

@rhuanjl I did some microbenchmarking and the original recursive case runs ~30% faster. Thanks for giving that a shot, and the quick turn-around.

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 7, 2018

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") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: !== is generally preferable

Copy link
Collaborator Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems reasonable to me, thanks!


In reply to: 208360161 [](ancestors = 208360161)

Copy link
Contributor

@sethbrenith sethbrenith left a comment

Choose a reason for hiding this comment

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

:shipit:

Copy link
Contributor

@boingoing boingoing left a 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
Copy link
Contributor

@dilijev dilijev Aug 10, 2018

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)

Copy link
Contributor

@dilijev dilijev left a 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 */) {
Copy link
Contributor

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 */) {
Copy link
Contributor

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

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 10, 2018

@dotnet-bot test OSX static_osx_osx_debug please

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 14, 2018

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?

@sigatrev
Copy link
Contributor

@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.

@sigatrev
Copy link
Contributor

@rhuanjl can you enable us to update the PR so I update bytecode headers and GUID?

@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 17, 2018

@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)

@chakrabot chakrabot merged commit 9de922b into chakra-core:master Aug 17, 2018
chakrabot pushed a commit that referenced this pull request Aug 17, 2018
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
@rhuanjl rhuanjl deleted the flat branch August 17, 2018 23:04
@rhuanjl
Copy link
Collaborator Author

rhuanjl commented Aug 17, 2018

Thanks for testing and merging this @sigatrev

And thanks everyone for the reviews.

@sigatrev
Copy link
Contributor

@rhuanjl Thanks for your patience. We really appreciate the contributions.

@dilijev dilijev added the Bytecode-Update This PR updates bytecode and will cause merge conflicts with other PRs with this label label Sep 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bytecode-Update This PR updates bytecode and will cause merge conflicts with other PRs with this label External Contributor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement Array.prototype.flatMap, Array.prototype.flat