-
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
SortCompare (Array#sort) #302
Comments
Not trying to be obtuse: what's the ECMA262 bug here? |
If SpiderMonkey and Chakra follow the spec, but v8 and JSC don't, then this is a v8 and a JSC bug. @Yaffle can you confirm that SpiderMonkey and Chakra follow the spec here? |
Also how does the first and second case differ? Maybe the first means to assign undefined rather than delete the property? |
Here's the results I get, with the update I suggest above: spidermonkeyUndefined elementsHolesHoles with prototype propertychakraUndefined elementsHolesHoles with prototype propertyd8Undefined elementscomparator was called for an undefined value Holescomparator was called for a hole Holes with prototype propertycomparator was called for a value from prototype Anyone using eshost can use the following script: print('##### Undefined elements');
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var counter = 0;
array.sort(function (a, b) {
if (counter === 0) {
for (var i = 0; i < array.length; i += 1) {
array[i] = undefined;
}
}
if (counter === 1) {
if (a == undefined || b == undefined) {
print("comparator was called for an undefined value");
}
}
counter += 1;
return a - b;
});
print('##### Holes');
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var counter = 0;
array.sort(function (a, b) {
if (counter === 0) {
for (var i = 0; i < array.length; i += 1) {
delete array[i];
}
}
if (counter === 1) {
if (a == undefined || b == undefined) {
print("comparator was called for a hole");
}
}
counter += 1;
return a - b;
});
print('##### Holes with prototype property');
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var counter = 0;
array.sort(function (a, b) {
if (counter === 0) {
for (var i = 0; i < array.length; i += 1) {
delete array[i];
Object.prototype[i.toString()] = 42;
}
}
if (counter === 1) {
if (a === 42 || b === 42) {
print("comparator was called for a value from prototype");
}
}
counter += 1;
return a - b;
}); |
@bterlson , the spec tells when and how to call
Well... not a big bug... |
@ljharb , SpiderMonkey may switch to "self-hosted" Array#sort ( https://bugzilla.mozilla.org/show_bug.cgi?id=715181 ) and there is a suggestion to use same behaviour for undefined values and holes as in V8 and JSC |
@Yaffle in other words, you are requesting a spec change to the v8/jsc approach? If so, can you expand on why it is necessary and/or desirable behavior? Otherwise this seems like a bug in V8 and JSC that SM should not copy... |
The special treatment of non-existent properties dates back to the ES3 spec. ES5 changes the language used to describe it from prose to the use of [[HasProperty]]. ES6 moved the test from inside of SortCompare into a guard on calling SortCompare. None of them call a user provided comparefn for non-existent properties. I don't recall the exact motivation for the ES6 refactoring but I'm pretty use that there are either es-discuss or bugs.ecmascript.org discussions that relate it it. Note that ES3 spec. has an explicit change from ES1&2 which did not explicitly check for non-existent properties and which explicitly stated that the handling of non-existent properties was implementation-dependent. Clearly, when ES3 was being developed a decision was made that undefined values and holes should not have the same treatment. In particular, that holes sort after undefined at the end of the array. I don't see why we would want to change that long standing design design. |
@allenwb, motivation might be that this semantics induces a non-zero runtime cost while having a zero real-world benefit. |
|
https://hg.mozilla.org/mozilla-central/rev/1c4b0a89fd5b - Firefox 47 does not use HasOwnProperty check and patches holes (note: the body of comparefn should not be equal to
|
@Yaffle that doesn't patch holes - I get |
@ljharb , the change was applied recently, do you use the latest nightly build? |
I used v46. On the latest, v47, I see the same behavior. |
somehow I see |
Ah, oops, yes I see the change you mean. 46 said "empty slot" but 47 says "undefined". Thanks. |
Using #302 (comment) and results#### Chakra
##### Undefined elements
##### Holes
##### Holes with prototype property
#### JavaScriptCore
##### Undefined elements
comparator was called for an undefined value
##### Holes
comparator was called for a hole
##### Holes with prototype property
comparator was called for a value from prototype
#### SpiderMonkey
##### Undefined elements
##### Holes
##### Holes with prototype property
#### V8
##### Undefined elements
comparator was called for an undefined value
##### Holes
comparator was called for a hole
##### Holes with prototype property
comparator was called for a value from prototype So it seems we're still at 50/50 - jsc and v8 do one thing, and chakra/spidermonkey do another. What do we want to do at this point? It seems like the options are:
It would be really helpful if implementors of any of the 4 browsers could weigh in on either their preference for one outcome, or their objection to one outcome. |
From reading the source code, XS appears to ignore the HasOwnProperty check, but because of Moddable-OpenSource/moddable#113 I can't really test this. As a fun sidenote, engine262 sides with SpiderMonkey and ChakraCore |
@ljharb, |
FWIW, some more background on V8's behavior w.r.t. holes during cc @schuay @Imasius |
Thanks @mathiasbynens , extracted a V8 bug for further investigation here: https://crbug.com/v8/8666 |
Looking into this with @schuay, we found that one of the requirements for consistent comparison functions is:
And inconsistent comparison functions are implementation-defined:
The test case in #302 (comment) exclusively uses inconsistent comparison functions, since it modifies the array during the first call. Can anyone think of a valid test case that shows a difference in implementations? |
Oh, I see. The subtlety here is in the part I quoted earlier:
It says the sort order is implementation-defined, but it doesn't explicitly say that anything else is implementation-defined too (such as whether or not Modified test case using consistent comparison functions: print('##### Undefined elements');
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
array.fill(undefined);
array.sort(function (a, b) {
if (a === undefined || b === undefined) {
print("comparator was called for an undefined value");
}
return a - b;
});
print('##### Holes');
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (var i = 0; i < array.length; i += 1) {
delete array[i];
}
array.sort(function (a, b) {
if (a === undefined || b === undefined) {
print("comparator was called for a hole");
}
return a - b;
});
print('##### Holes with prototype property');
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (var i = 0; i < array.length; i += 1) {
delete array[i];
Object.prototype[i] = 42;
}
array.sort(function (a, b) {
if (a === 42 || b === 42) {
print("comparator was called for a value from prototype");
}
return a - b;
}); Results:
V8 has the correct behavior for consistent comparison functions. |
What does this part of the spec mean?
The use of "previous call" and the past tense is confusing. Should that just be "call"? Is the intention to perform prototype chain lookups in these cases (i.e. to print In case of the latter, I'd suggest the following change:
|
My reading of the current paragraph seems to say that once it's been observed that a property exists or doesn't, or is own or isn't, that it can't change? By removing "previous" the timeline of "when a change matters" seems to become unspecified. |
From an implementer POV, being able to avoid prototype lookups would be so great. But why not just spec it with [[GetOwnProperty]] in that case? |
I think a clarification of the exact time when this |
@szuend have you been able to look into this more? |
Somewhat. For V8 we decided to change the sort pre-processing. V8 walks all indices from [0, [[length]]) and if HasProperty returns true, call [[Get]] and add the element to a temporary list. Sorting is then done on this temporary list. After sorting elements are written back using [[Set]] (and calling DeleteProperty in case there were holes). This approach has the advantage that the interaction with the prototype chain and potential accessors is well-defined and no longer depends on the sorting algorithm used. |
Closing, since #1585 landed. |
http://tc39.github.io/ecma262/#sec-sortcompare
According to spec
comparefn
will be never called for undefined values, but v8 and WebKit implementations do not implement this behavior:The spec only tells that the
sort order
is implementation defined...v8 and WebKit do this for performance reasons, probably, because they can move undefined values and holes to the end of an array before the sorting and call
comparefn
directly:https://github.com/v8/v8/blob/master/src/js/array.js#L1194
https://github.com/WebKit/webkit/blob/cc80ecf5f0902e724a6e398d152323120e4e3b82/Source/JavaScriptCore/builtins/ArrayPrototype.js#L473
The text was updated successfully, but these errors were encountered: