Changing behavior of Array#copyWithin when used on array-like with negative length
ToLength is used a number of places within the ES6 spec. where formerly ToUint32 was used. It allows indices to be larger 2^32-2 and avoids weird wrap behavior for indices in that range. I doubt that we could compatibly get away with replacing those legacy ToUnit32 calls with a ToLength that preserved negative values. Even if we could we would have to review all of the array (and string) algorithms that use ToLength to make sure they still work reasonably with negative length values. I really don't see what benefit we would get from that work.
I think Scott is requesting this change: gist.github.com/anba/6c75c34c72d4ffaa8de7
Le 14 févr. 2014 à 21:46, "C. Scott Ananian" <ecmascript at cscott.net> a écrit :
array-likes with negative length
"Array-likes with negative length" doesn't make sense, or at least it isn't a useful concept as far as ECMAScript is concerned – as it doesn't make sense to consider arraylikes of fractional length, or of length equal to the string "LOL".
If the informal signature of Array.prototype.copyWithin is misleading, it could be rewritten as:
Array.prototype.copyWithin (target, start, end = length of this)
where "length of obj" is to be interpreted as ToLength(obj.length) – at least, it is what the algorithm uses in each and every step where the "length" is expected (the len variable in steps 8, 11, 12, 14 and 15).
On Fri, Feb 14, 2014 at 11:50 AM, André Bargull <andre.bargull at udo.edu> wrote:
I think Scott is requesting this change: gist.github.com/anba/6c75c34c72d4ffaa8de7
Yes, although my proposed diff (in the linked bug) was the shorter, "12. If end is undefined, let relativeEnd be ToInteger(lenVal); else let relativeEnd be ToInteger(end)." Same effect, though.
Claude Pache wrote:
"Array-likes with negative length" doesn't make sense.
Array.prototype.copyWithin.call({ length: -1 }, ... );
Call it whatever you like, although I'm always interested in learning new phrases of ECMAspeak (if there's an appropriate name for this).
Le 14 févr. 2014 à 23:40, "C. Scott Ananian" <ecmascript at cscott.net> a écrit :
Call it whatever you like, although I'm always interested in learning new phrases of ECMAspeak (if there's an appropriate name for this).
Since you are interested: "The abstract operation ToLength converts its argument to an integer suitable for use as the length of an array-like object." 1
It doesn't really matter whether ToLength could extract a reasonable value for an unreasonable object, or not. But consistency is important: note that this operation is used whenever the length of an arraylike is expected, not only for default arguments.
Would "an array-like with a negative length field" read better in
ecmaspeak? I'm talking about the value before ToLength is invoked.
Array.prototype.slice.call(3) and
Array.prototype.slice.call({length:-1}) both complete successfully (and
return the same value), due to the soothing effect of ToLength. My
informal definition for "array-like" is "something which is not actually an
Array but which will not throw an error when used as the receiver in an
invocation of a generic method of Array", so both 3 and {length:-1} are
"array-like" under my definition, and then I need a way to describe the
difference between them. I believe you are saying they are both
"array-likes with zero length", since that's the result of ToLength. But
perhaps you wouldn't call them "array-like" at all?
On Fri, Feb 14, 2014 at 12:40 PM, C. Scott Ananian <ecmascript at cscott.net> wrote:
Yes, although my proposed diff (in the linked bug) was the shorter, "12. If end is undefined, let relativeEnd be ToInteger(lenVal); else let relativeEnd be ToInteger(end)." Same effect, though.
No objections from implementors? Has anyone actually implemented
copyWithin (other than in es6-shim)?
Le 14 févr. 2014 à 23:40, C. Scott Ananian <ecmascript at cscott.net> a écrit :
Yes, although my proposed diff (in the linked bug) was the shorter, "12. If end is undefined, let relativeEnd be ToInteger(lenVal); else let relativeEnd be ToInteger(end)." Same effect, though.
Just a last note. Beyond the philosophical aspect whether arraylikes of negative length make any sense at all, there is a strong technical issue you have probably overlooked: For array methods in general, and for the optional argument of Array.prototype.copyWithin in particular (see step 14 of the algorithm), a negative index is not understood as an absolute position, but rather as a position relative to the end of the array. For instance, for an array (or arraylike) of length n, if -3 is passed, it indicates position n-3. In your case, it is certainly not the semantics you want. (In fact, everything will happily coerce to 0 at the end of the journey, but it's just happenstance.)
I have not overlooked it. The fact that negative end is significant
is exactly the reason why using ToLength on it seems wrong. Even
thought end defaults to this.length, it should still be normalized
with ToInteger since negative values are semantically valid.
But as you point out, I don't think there's any actual behavior
change, since everything washes out to 0 at the end. It's just a
matter of writing a clearer more consistent spec.
C. Scott Ananian wrote:
But as you point out, I don't think there's any actual behavior change, since everything washes out to
0at the end. It's just a matter of writing a clearer more consistent spec.
Yet in that light you still have a point, I think. Allen?
On 2/14/2014 11:40 PM, C. Scott Ananian wrote:
Yes, although my proposed diff (in the linked bug) was the shorter, "12. If end is undefined, let relativeEnd be ToInteger(lenVal); else let relativeEnd be ToInteger(end)." Same effect, though.
It's not the same effect, because lenVal could be an object with
valueOf/toString/@toPrimitive side-effects.
On Feb 17, 2014, at 12:25 PM, Brendan Eich wrote:
Yet in that light you still have a point, I think. Allen?
The ticket is still open, but it really is a style issue and if a change is made here consistency probably requires changes to other methods as well. Right now I have bigger fish to finish frying but at some point I'll consider it.
It's not the same effect, because
lenValcould be an object with valueOf/toString/@toPrimitive side-effects.
Point taken. (Although I'm fine with invoking the side effects twice
if you're using this.length as a default value, since that would be
'unsurprising' if you are looking at the method signature.)
Allen: I can volunteer to offload some of the work of auditing for
similar cases with default arguments. From a quick read-through, only
Array#fill seems to have the same issue. Array#lastIndexOf is
written in the ES5 style, where we look at arguments.length instead
of using default parameters in the signature. But it does use
ToLength(this.length) - 1 as a default value; I think that could be
changed to ToInteger(this.length) - 1 for consistency without
affecting actual behavior.
The copyWithin, fill, and lastIndexOf, subarray methods of
%TypedArray%.prototype are spec'ed with default arguments equal to
this.length, but there's no way length can be negative there, I
think (offtopic: ...although I'd really be much happier if the
generic Array methods could be written to special-case %TypedArray% in
the proper way so that we could observe DRY instead of cloning most of
the algorithm descriptions).
Le 17 févr. 2014 à 21:25, Brendan Eich <brendan at mozilla.com> a écrit :
Yet in that light you still have a point, I think. Allen?
Different perspectives... My point of view is precisely that, with the change proposed, the algorithm is less clear, and less consistent, for the reasons stated in this thread. The benefit of the change is that the signature Array.prototype.copyWithin (target, start, end = this.length) doesn't lie for silly values of this. Is it worth the added obscurity and inconsistency of the algorithm itself for these same cases?
But I think that clarity can be improved by avoiding to obscure the intent by computation details. For my personal polyfill, I have found useful to abstract out the following steps, used (with slight variations) thrice in Array.prototype.copyWithin and twice in Array.prototype.fill, (and which can be used for various other methods of Array.prototype):
IndexFromRelativeIndex(k, len, default)
- Assert:
lenis an integer between 0 and 2^53-1. - Assert:
defaultis an integer between 0 andlen. - If
kisundefined, returndefault - Let
relativebe ToInteger(k). ReturnIfAbrupt(relative). - If
relative>= 0, return min(relative,len). - Else, if
relative< 0, return max(len+relative, 0).
I think that the intent of this algorithm is clear despite its Cobol-like language. Actually, for step 3, the spec uses the equivalent of: "If k is undefined, let relative be default and go to step 5"; moreover, that step is omitted when default is 0. The places where Array.prototype.copyWithin 1 uses it, are (where len = ToLength(this.length) per step 4):
steps 6 to 8. Let to be IndexFromRelativeIndex(target, len, 0). ReturnIfAbrupt(to).
steps 9 to 11: Let from be IndexFromRelativeIndex(start, len, 0). ReturnIfAbrupt(from).
steps 12 to 14: Let final be IndexFromRelativeIndex(end, len, len). ReturnIfAbrupt(final).
Note in particular that the default value is always one of the two ends of the range of possible positions, either 0 or len.
Le 17 févr. 2014 à 22:32, C. Scott Ananian <ecmascript at cscott.net> a écrit :
Allen: I can volunteer to offload some of the work of auditing for similar cases with default arguments. From a quick read-through, only
Array#fillseems to have the same issue.Array#lastIndexOfis written in the ES5 style, where we look atarguments.lengthinstead of using default parameters in the signature. But it does useToLength(this.length) - 1as a default value; I think that could be changed toToInteger(this.length) - 1for consistency without affecting actual behavior.
There is also Array#slice, which uses ToLength(this.length) as default for its second argument (even if its length is 2).
I like this refactoring. This doesn't change the spec's behavior;
this would be the first solution proposed in
ecmascript#2546 which was to rewrite
the spec's non-normative text to make it clear that end=this.length
in the signature was describing the behavior in the common case only.
The description of Array#slice for example, does not use a default
parameter in the signature and is accurate (assuming you read "the
length of the array" as ToLength(this.length)).
On Tue, Feb 18, 2014 at 10:32 AM, Claude Pache <claude.pache at gmail.com> wrote:
I think that the intent of this algorithm is clear despite its Cobol-like language. Actually, for step 3, the spec uses the equivalent of: "If
kisundefined, letrelativebedefaultand go to step 5"; moreover, that step is omitted whendefaultis 0.
This difference seem to have no effect, given that step 2 asserts that
default is an integer between 0 and len.
steps 6 to 8. Let
tobe IndexFromRelativeIndex(target,len, 0). ReturnIfAbrupt(to). steps 9 to 11: Letfrombe IndexFromRelativeIndex(start,len, 0). ReturnIfAbrupt(from).
For greater precision, you might want to specify +0 as the default
here, to match the result specified for ToInteger(undefined).
Claude Pache wrote:
But I think that clarity can be improved by avoiding to obscure the intent by computation details. For my personal polyfill, I have found useful to abstract out the following steps, used (with slight variations) thrice in
Array.prototype.copyWithinand twice inArray.prototype.fill, (and which can be used for various other methods ofArray.prototype):IndexFromRelativeIndex(
k,len,default)
+1
Nit: How about just FromRelativeIndex for the name?
For reference: ecmascript#2546
Array#copyWithinhas a (non-normative) signature of(target, start, end = this.length). However, this is slightly misleading because the spec actually callsToLengthonthis.lengthand then uses that for the default value ofend. This changesend's effective value whenthis.lengthis negative.In the bug we discuss changing the non-normative descriptive text to be less misleading.
But I'd like to invite broader discussion on a different approach: can we change the spec so that the
end = this.lengthdefault was actually correct? This would only possibly change behavior on array-likes with negative length, and probably wouldn't even change observable behavior in that case (sincelengthis treated as 0). Basically we'd be just callingToIntegeron the default value ofendrather thanToLength. But it would be an end-run around confusing language in the spec.