-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Normative: A more precise Array.prototype.sort #1585
Conversation
spec.html
Outdated
@@ -32650,9 +32638,6 @@ <h1>Array.prototype.sort ( _comparefn_ )</h1> | |||
<h1>Runtime Semantics: SortCompare ( _x_, _y_ )</h1> | |||
<p>The SortCompare abstract operation is called with two arguments _x_ and _y_. It also has access to the _comparefn_ argument passed to the current invocation of the `sort` method. The following steps are taken:</p> | |||
<emu-alg> | |||
1. If _x_ and _y_ are both *undefined*, return *+0*. | |||
1. If _x_ is *undefined*, return 1. | |||
1. If _y_ is *undefined*, return -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.
undefined
s are never part of items
, and thus, x
and y
can never be undefined
.
To make this easier to follow, Simon left inline comments on the diff. We've also prepared a slide deck that walks through the proposed algorithm in a more visual way, which I'll present at the upcoming TC39 meeting in July. One question people might have is: to which extent does this PR match implementation reality? We mentioned that V8 implements
|
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’d be very interested in seeing the toString step included as well, to reduce variance further.
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.
Since %TypedArray%.prototype.sort
is defined roughly as a diff against Array.prototype.sort
, it may need collateral changes.
For instance, this sentence would no longer apply?:
The implementation-defined sort order condition for exotic objects is not applied by %TypedArray%
.prototype.sort
.
Also, its reference to "the entry steps in Array.prototype.sort" would be a big vaguer, since those steps no longer constitute a distinct <emu-alg>
of their own. We could just leave it as is and trust that people will figure it out, or change it to something like "the first three steps in Array.prototype.sort", but then that's less robust to future change (e.g. if A.p.sort were to insert some Asserts at the start).
I'd like to see more investigation of the behavior of existing engines in edge cases. Off the top of my head:
I am sure there are more cases to consider, too. |
SpiderMonkey always first collects all elements, stores them in a temporary object, and then performs the search in that temporary object:
The elements in the sorted object are then written back with [[Set]] semantics and holes are deleted through [[Delete]]: |
I implemented some of these here. All tests were run using Accessor on the array itself (
Accessors on the arrays prototype object with a hole (
non-writeable or non-configurable elements on the array or its prototype
Accessors mutate an element
Accessors delete an element
Accessor add/remove elements
Accessors modify
0-length and 1-length arrays (
Sort on a proxy with a backing array
Sort an array with a proxy prototype
The result is not that surprising given that v8 implements basically the same approach now as spidermonkey. Please note that in the |
Thanks for the more thorough tests! It looks like JavaScriptCore disagrees with the proposed spec more often than it agrees, so this normative change mostly amounts to a request that JSC change their implementation of |
@kmiller68 @msaboff During the last meeting I had the impression you were willing to change the JSC implementation. Is this still the case? (We could also discuss this during the meeting, but if this turns out to be uncontroversial, then we could all save some valuable meeting time.) |
I've filed https://bugzilla.mozilla.org/show_bug.cgi?id=1583528 for the necessary SpiderMonkey changes. |
Per the recent TC39 meeting, this patch now has consensus. Here's what still needs to happen before it can be merged:
|
I’ve filed https://bugs.webkit.org/show_bug.cgi?id=202582 to track the necessary JSC changes. I’ll also update this thread with the results of my investigation. |
Here are a few bugs:
That invariant is now broken.
That invariant is now also broken due to the refactoring of SortCompare to take out the handling of
It's not clear what "compare" means here, but this is in the SortCompare section, so the presumable interpretation is "SortCompare". In that case, this is no longer valid due to the |
Could you please elaborate here? I don't see how the invariant is broken.
We can revert the refactoring of upfront |
spec.html
Outdated
@@ -33717,14 +33693,13 @@ <h1>%TypedArray%.prototype.some ( _callbackfn_ [ , _thisArg_ ] )</h1> | |||
<h1>%TypedArray%.prototype.sort ( _comparefn_ )</h1> | |||
<p>%TypedArray%`.prototype.sort` is a distinct function that, except as described below, implements the same requirements as those of `Array.prototype.sort` as defined in <emu-xref href="#sec-array.prototype.sort"></emu-xref>. The implementation of the %TypedArray%`.prototype.sort` specification may be optimized with the knowledge that the *this* value is an object that has a fixed length and whose <emu-xref href="#integer-index">integer-indexed</emu-xref> properties are not sparse. The only internal methods of the *this* value that the algorithm may call are [[Get]] and [[Set]].</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.
The only internal methods of the this value that the algorithm may call are [[Get]] and [[Set]].
Isn't this violated by these changes? It might be good to abstract out the shared bit into an abstract op which both methods can call, and put the hole removal logic in the array one only.
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.
The hole deletion loop doesn't get hit for TypedArrays, as far as I can tell, so we shouldn't have to worry about that part.
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 we should remove the
The only internal methods of the this value that the algorithm may call are [[Get]] and [[Set]].
bit, since that was only relevant when implementations had considerably more freedom for how to do the sort. Now that the reading and writing parts are fully specified it's not relevant.
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.
Whatever way it goes, it would still be nice to use an abstract op instead of magic step replacement.
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've made @bakkot's suggested change. W.r.t. using an abstract operation, I agree, but since it's orthogonal to this PR (step replacement was already happening before) I'd prefer to tackle it separately.
Co-authored-by: Simon Zünd <szuend@chromium.org> Co-authored-by: Mathias Bynens <mathias@qiwi.be>
Thanks @szuend and @mathiasbynens for making this happen! |
`Array.prototype.sort` was already made stable in ECMAScript 2019 (see two paragraphs above). The change in tc39#1585 made `Array.prototype.sort` more precise, reducing the amount of cases that result in an implementation-defined sort order.
`Array.prototype.sort` was already made stable in ECMAScript 2019 (see two paragraphs above). The change in tc39#1585 made `Array.prototype.sort` more precise, reducing the amount of cases that result in an implementation-defined sort order.
`Array.prototype.sort` was already made stable in ECMAScript 2019 (see two paragraphs above). The change in #1585 made `Array.prototype.sort` more precise, reducing the amount of cases that result in an implementation-defined sort order.
… r=tcampbell <tc39/ecma262#1585> changed `Array.prototype.sort` to first collect all elements in a temporary list. And because `TypedArray.prototype.sort` follows `Array.prototype.sort`, the same copying has to happen here. Depends on D143288 Differential Revision: https://phabricator.services.mozilla.com/D143289
… r=tcampbell <tc39/ecma262#1585> changed `Array.prototype.sort` to first collect all elements in a temporary list. And because `TypedArray.prototype.sort` follows `Array.prototype.sort`, the same copying has to happen here. Depends on D143288 Differential Revision: https://phabricator.services.mozilla.com/D143289
… r=tcampbell <tc39/ecma262#1585> changed `Array.prototype.sort` to first collect all elements in a temporary list. And because `TypedArray.prototype.sort` follows `Array.prototype.sort`, the same copying has to happen here. Depends on D143288 Differential Revision: https://phabricator.services.mozilla.com/D143289
… r=tcampbell <tc39/ecma262#1585> changed `Array.prototype.sort` to first collect all elements in a temporary list. And because `TypedArray.prototype.sort` follows `Array.prototype.sort`, the same copying has to happen here. Depends on D143288 Differential Revision: https://phabricator.services.mozilla.com/D143289
tl;dr This PR intends to nail down some parts of
Array.prototype.sort
to reduce the amount of cases that result in an implementation-defined sort order.Overview
The
Array.prototype.sort
procedure that this PR proposes, can be summarized as follows:undefined
values in the range of[0, [[length]])
into a temporary list using[[Get]]
(let the length of this list ben
).undefined
s are counted and not added to this temporary list (let this count bem
).n
sorted values using[[Set]]
.m
undefined
s.[[Delete]]
on integer-indexed properties in the range of[n + m, [[length]])
as holes are moved to the end of the sorting range.Advantages
The main advantage is that the following cases no longer result in an implementation-defined sort order:
3
whenObject.prototype[3] = 42
)[[Get]]
,[[Set]]
behavior for integer-indexed properties inside the sorting range is not the "ordinary" implementation (i.e. a getter or setter or a proxy).The reason for this is that
[[Get]]
,[[Set]]
and[[Delete]]
are now called in a well-defined order, regardless of the chosen sorting algorithm of any engine. (Previously, non-extensible objects and non-configurable/non-writable properties could result in exceptions at any point in time during sorting, depending on the implementation.)Additionally,
ToString
operations or comparison functions that throw now leave the object as-is (modulo side-effects and changes caused by[[Get]]
).Web compatibility
We believe this change to be web-compatible, given that it has been (for the most part) shipping since Chrome 74, with the remaining bits shipping since Chrome 76.
Starting with V8 v7.4 (Chrome 74), V8 copies non-
undefined
values into a temporary list for sorting, as described in this PR. However, compacting all values at the start of the sorting range (removing holes) still happened on the object itself and was observable.As of V8 v7.6 (Chrome version 76.0.3806.0) V8's implementation of
Array.prototype.sort
fully behaves as described by this PR.Memory concerns
Implementers might be concerned about the added
O(n)
memory requirements ofArray.prototype.sort
. The first version in V8 that appliedArray.prototype.sort
to a temporary copy landed with Chrome 74. Worried about the impact on memory consumption (especially on mobile), we analysed memory usage of some 1000 websites and did not see any change in peak memory consumption.Variants
This is only a first initial draft of how
Array.prototype.sort
could be specified in a more precise manner. A few variations come to mind:undefined
s, they could be collected as well and passed toSortCompare
. This would requireSortCompare
to be left as-is and handleundefined
s appropriately.undefined
comparison function, the initial collection phase could callToString
directly. This means the temporary list would contain pairs consisting of the original value and the result ofToString
for the respective value. This would further reduce the cases of implementation-defined sort order.Ref. #302.
@mathiasbynens @ajklein