7

Edit: Simpler repro case; the following code:

setInterval(function(){
  var a=[10,20,30,40], i=-1;
  a[-1] = 42;
  while (i<10000) a[i++];
  console.log(a[-1],a[4294967295]);
},100);

…produces the output:

     42 undefined
     undefined 42
     42 undefined
37x  undefined 42
     42 undefined
     undefined 42
     42 undefined
41x  undefined 42
     42 undefined
     undefined 42
     42 undefined

Try it yourself: http://jsfiddle.net/Fjwsg/


(Original question follows)

Given the following code (or code like it (fiddle)):

<!DOCTYPE HTML>
<html><head><title>-1 Array Index</title></head><body>
  <label>p: <input id="p" size="3"></label>
  <script type="text/javascript">
  var p = document.getElementById('p');
  p.onkeyup = function(){
    var a = "10 20 30 40".split(/\s+/);
    foo(a, p.value*1);
  }

  function foo(a,p){
    var count=a.length, i=0, x;
    if (p) a[i=-1]=p;
    while (i<10000) x = a[i++ % count];
    console.dir(a);
  }
  </script>
</body></html>

I see the following in the Developer Console when I focus the "p" input and type 1 backspace 2:
Array has index of -1 on first pass, index of 4294967295 on third pass

Once I see the index of 4294967295 (232 - 1) show up, things sometimes begin to "go bad": sometimes the Developer Tools automatically close, sometimes all Safari tabs freeze and require a relaunch to recover.

Oddly, I can't repro this if I remove the (largely useless in this case) while loop. And I can't repro this on Chrome or Firefox.

Can anyone shed any light on what could possibly be at the root of this problem?

This is occurring with Safari 5.1.7 on OS X 10.7.4

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • I'm still looking this over but the while loop doesn't actually do "nothing", it increments the `i` variable. – Elliot Bonneville May 17 '12 at 02:22
  • [Unable to reproduce](http://jsfiddle.net/TzCm9/) in Safari 5.1.4 on a Windows 7 machine. When I type `1` I get length 4, backspace I get length 4, and when I type `2` I get length 4. It's also setting `a[-1]` to equal 1 and then 2 on the appropriate keypresses, which is kind of impossible. I'm stumped. – Elliot Bonneville May 17 '12 at 02:31
  • In Safari 5.1.6 OS 10.7.4 I get the weird index, but nothing freezes. – bfavaretto May 17 '12 at 02:35
  • Do you mean `if (p) a[i=-1]=p;` or `if (p) a[i -= 1]=p;`. – Andrew May 17 '12 at 03:12
  • 1
    If you actually intend to use a negative index, change the index to strings and use an object as the storage rather than array. – Andrew May 17 '12 at 03:17
  • @Andrew The former; I have an array that I loop through, but on the first pass just once I needed to inject one extra value. I can do it another way, and will, but that's what led to this. – Phrogz May 17 '12 at 04:28
  • 1
    @Phrogz I can't remember where I read this, but Douglas Crockford said that JavaScript's `Array` is actually just a regular `Object` with an extra `length` property, which is always 1 greater than the largest numeric index in the object, and that JS uses type coercion to change the numeric index to a string during lookups and insertions. There's obviously a bit more happening under the hood, but it means you don't lose much by keeping what you have now and going to a `{}` instead of a `[]`. Anyway, in other languages you can't use neg. indexes because the index is a pointer to the heap. – Andrew May 17 '12 at 05:18
  • @Andrew that sounds about right, the ecma spec says that the index cannot be 2^32-1 (also in my answer) because that value is reserved for the `.length` property before it overflows. – Ja͢ck May 17 '12 at 09:55
  • @Andrew Note that `a=[]; a[1]="foo"; a["2"]="bar"; a["1"]=="foo" && a[2]=="bar" // true`. All JavaScript properties are equivalent to strings internally--even array indices--so _"chang[ing] the index to strings"_ would not effect the result. – Phrogz May 17 '12 at 14:59
  • @Jack That's the most interesting thing about JS I've read in a long while. It makes sense, but I honestly can't help feeling like that's just a cheap work-around. It's hard to believe that's in an RFC. – Andrew May 17 '12 at 15:08
  • @Phrogz With all the optimization that's gone into modern JS engines, I'd be reluctant to say that there'd be no difference at all in string-based indexes vs. numerical indexes. In strongly typed languages, numeric indexes are typically *much* faster than hashes. On the other hand, an added step (for number-to-string coercion) will barely but measurably slow things down. I haven't a clue how it would shake down at the end, but it would be interesting to test which is faster in various browsers. – Andrew May 17 '12 at 15:16
  • @Andrew Yes, there is [a performance difference](http://jsperf.com/string-vs-integer-array-indices) for sure. – Phrogz May 17 '12 at 15:31

2 Answers2

3

Using negative indices may result in undefined behaviour.

According to the ECMAScript documentation, a particular value p can only be an array index if and only if:

(p >>> 0 === p) && (p >>> 0 !== Math.pow(2, 32) - 1)

In your case:

# -1 >>> 0
4294967295
# Math.pow(2, 32) - 1
4294967295

edit

If p fails above test, it should be treated as a regular object property like a.foo or a['foo']. That said, as it turns out, setting the negative index with a string cast works around the issue.

a['' + (i =-1)] = p

Conclusion: browser bug

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • I appreciate your link, but I think the nomenclature used by the specification is confusing you. Array--like all objects in JavaScript--can have any number of arbitrary properties added to them at runtime. The specification simply constrains which properties are considered an "Array Index" for the purposes of auto-updating the `length` property. It is perfectly valid to add a property `foo` or `lastSeen` or `"OMG!"` to an array. – Phrogz May 17 '12 at 14:40
  • @Phrogz I'm aware that Array objects can be extended at runtime, so I guess the second part of my answer could be improved ... the first part is still valid though; wondering why this got downvoted =/ – Ja͢ck May 17 '12 at 15:15
  • I downvoted it. The first part of your answer is correct (citing specs accurately) but a red herring. Setting a property named `"-1"` is no more "naughty" than setting a property named `"yo' momma"`: both are acceptable per the specs, and simply will not be considered an "array index" (and all that entails). (And your first statement is sort of begging the question: other than this bug on what browser on what platform, can you cite any other cases where negative indices result in "unexpected behavior"?) – Phrogz May 17 '12 at 15:17
  • @Phrogz there's a difference between a["-1"] and a[-1] though. Are you saying that the JavaScript engine should convert the invalid index to a string, thereby avoiding the problem you were seeing? – Ja͢ck May 17 '12 at 15:22
  • 1
    @Phrogz you're right about the 'results in unexpected behaviour' .. I've updated that; sorry, English is not my first language, so what I think and write are not always the same :) – Ja͢ck May 17 '12 at 15:26
  • `a[-1]="foo"; a["-1"]=="foo"; a["-2"]="bar"; a[-.2e1]=="bar"` – Phrogz May 17 '12 at 15:49
  • `var a = [1, 2, 3]; a[-1] = 'wat'; a.length == 3; for (var x in a) { console.log(a[x]); // 1, 2, 3, wat` ... it's just better avoided imho. btw, did you try the same script but cast the -1 to string? – Ja͢ck May 17 '12 at 16:15
  • I did, and found that setting the index as a string `a["-1"] = …` does, in fact, work around this bug (!) – Phrogz May 17 '12 at 22:23
  • @phrogz thanks for the verification. Answer updated to reflect this. – Ja͢ck May 17 '12 at 23:39
2

This sounds like a bug in JavaScriptCore to me. Perhaps foo is being JITted when it's called the second time and the JITted code introduces the bug.

I'd recommend filing a bug against JavaScriptCore and attaching your test case.

Adam Roben
  • 754
  • 4
  • 6
  • Thank you for the suggestion and possible explanation. I've filed this as [bug #86733](https://bugs.webkit.org/show_bug.cgi?id=86733). – Phrogz May 17 '12 at 14:51