dart-sdk/tests/web/nan_index_test.dart
Stephen Adams 5c582f82f0 [dart2js, js_runtime, js_dev_runtime] NaN-safe range checks.
`int` variables can attain NaN values because web int arithmetic
implemented by JavaScript numbers (doubles) is not closed under many
operations. It is possible get NaN using only addition:

    int a = 1, b = -1;
    while (a + a != a) { a += a; b += b; }
    int nan = a + b;

On the VM, a, b and nan are all zero.
On the web, a, b and nan are Infinity, -Infinity and NaN, respectively.

Since NaN can leak into int arithmetic, is it helpful if bounds checks
catch NaN indexes. NaN compares false in any comparison, so a test
of the form

   if (index < 0 || index >= a.length) throw ioore(a, index);

fails to detect a NaN value of `index`.
This is fixed by negating the comparisons, and applying De Morgan's law:

   if (!(index >= 0 && index < a.length)) throw ioore(a, index);

These changes have been applied to JSArray.[], JSArray.[]= and String.[]

For dart2js the change is a little more involved. Primitive indexing is
lowered to code with a HBoundsCheck check instruction. The code generated
for the instruction now uses, e.g. `!(i>=0)` instead of `i<0`.
This leads to a small code size regression.

There is no regression at -O4 since bounds checks are omitted at -O4.

At -O3 (where the regression is largest) the regression is
   0.01% for cm
   0.06% for flutter gallery -- array-heavy diff and layout
   0.21% for Meteor          -- array-heavy code
   0.30% for Box2DOctane     -- array-heavy code

I believe the regression can be largely alleviated by determining if
NaN is impossible at the index check, and if so, reverting to the smaller
code pattern. The analysis could be global, incorporating NaN into the
global abstract value domain, or a much simpler a local dataflow
analysis. Many indexes are loop driven and cannot reach infinity because
they are incremented by a small bump and eventually (even without a loop
guard) the index would stop growing when the increment falls below the
rounding error in O(2^53) iterations.


Change-Id: I23ab1eb779f1d0c9c6655e13d69f65d453db9284
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/210321
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
2021-08-27 00:37:56 +00:00

117 lines
3.8 KiB
Dart

// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:typed_data';
import 'package:expect/expect.dart';
// Check that NaN is detected as an invalid index for system Lists, Strings and
// typed lists.
//
// There are several methods called `dynamicCallN` for various N that are called
// with different indexable collection implementations to exercise various
// dart2js optimizations based on knowing (or not knowing) the concrete type of
// the List argument.
void main() {
int nan = makeIntNaN();
Expect.isFalse(nan <= 0);
Expect.isFalse(nan >= 0);
List<int> ints = [1, 2, 3, 4];
final bytes = Uint8List(3)
..[0] = 100
..[1] = 101
..[2] = 102;
final words = Int16List(3)
..[0] = 16000
..[1] = 16001
..[2] = 16002;
Expect.throws(() => ints[nan], anyError, 'List[nan]');
Expect.throws(() => 'abc'[nan], anyError, 'String[nan]');
Expect.throws(() => bytes[nan], anyError, 'UInt8List[nan]');
Expect.throws(() => words[nan], anyError, 'Int16List[nan]');
// [dynamicCall1] Seeded with JSIndexable and Map, so is doing a complete
// interceptor dispatch.
Expect.equals(2, dynamicCall1(ints, 1));
Expect.equals('b', dynamicCall1('abc', 1));
Expect.equals(2, dynamicCall1({'a': 1, 'b': 2, 'c': 3}, 'b'));
Expect.throws(() => dynamicCall1(ints, nan), anyError, 'dynamic List');
Expect.throws(() => dynamicCall1('AB', nan), anyError, 'dynamic String');
Expect.throws(() => dynamicCall1(bytes, nan), anyError, 'dynamic Uint8List');
Expect.throws(() => dynamicCall1(words, nan), anyError, 'dynamic Int16list');
var a = <int>[];
Expect.throws(() => a.removeLast(), contains('-1'));
// [dynamicCall2] seeded with JSIndexable only, so can be optimized to a
// JavaScript indexing operation.
Expect.equals(2, dynamicCall2(ints, 1));
Expect.equals('b', dynamicCall2('abc', 1));
Expect.throws(() => dynamicCall2(ints, nan), anyError, 'JSIndexable List');
Expect.throws(() => dynamicCall2('AB', nan), anyError, 'JSIndexable String');
// [dynamicCall3] Seeded with List of known length only, various indexes. The
// upper bound is fixed.
Expect.throws(() => dynamicCall3(ints, nan), anyError, 'known length nan');
Expect.throws(() => dynamicCall3(ints, null), anyError, 'known length null');
// [dynamicCall4] Seeded with List of known length only.
Expect.throws(() => dynamicCall4(ints, nan), anyError, 'dynamic[] List');
// [dynamicCall5] Seeded with List of unknown length only.
Expect.throws(() => dynamicCall5(ints, nan), anyError, 'dynamic[] List');
Expect.throws(() => dynamicCall5(a, nan), anyError, 'dynamic[] List');
// [dynamicCall6] Seeded with Uint8List only.
Expect.throws(() => dynamicCall6(bytes, nan), anyError, 'dynamic Uint8List');
}
bool anyError(error) => true;
bool Function(dynamic) contains(Pattern pattern) =>
(error) => '$error'.contains(pattern);
@pragma('dart2js:noInline')
dynamic dynamicCall1(dynamic indexable, dynamic index) {
return indexable[index];
}
@pragma('dart2js:noInline')
dynamic dynamicCall2(dynamic indexable, dynamic index) {
return indexable[index];
}
@pragma('dart2js:noInline')
dynamic dynamicCall3(dynamic indexable, dynamic index) {
return indexable[index];
}
@pragma('dart2js:noInline')
dynamic dynamicCall4(dynamic indexable, dynamic index) {
return indexable[index];
}
@pragma('dart2js:noInline')
dynamic dynamicCall5(dynamic indexable, dynamic index) {
return indexable[index];
}
@pragma('dart2js:noInline')
dynamic dynamicCall6(dynamic indexable, dynamic index) {
return indexable[index];
}
int makeIntNaN() {
int n = 2;
// Overflow to Infinity.
for (int i = 0; i < 10; i++, n *= n) {}
// Infinity - Infinity = NaN.
return n - n;
}