Files
sdk/pkg/compiler/test/codegen/value_range_test.dart
T
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

302 lines
5.4 KiB
Dart

// Copyright (c) 2017, 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.
// @dart = 2.7
import 'dart:async';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import '../helpers/compiler_helper.dart';
const int REMOVED = 0;
const int ABOVE_ZERO = 1;
const int BELOW_LENGTH = 2;
const int KEPT = 3;
const int ONE_CHECK = 4;
const int ONE_ZERO_CHECK = 5;
const int BELOW_ZERO_CHECK = 6;
final List TESTS = [
"""
main() {
var a = new List();
var sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i];
}
return sum;
}
""",
REMOVED,
"""
main(value) {
var a = new List();
var sum = 0;
for (int i = 0; i < value; i++) {
sum += a[i];
}
return sum;
}
""",
ABOVE_ZERO,
"""
main(check) {
// Make sure value is an int.
var value = check ? 42 : 54;
var a = new List(value);
var sum = 0;
for (int i = 0; i < value; i++) {
sum += a[i];
}
return sum;
}
""",
REMOVED,
"""
main() {
var a = new List();
return a[0];
}
""",
KEPT,
"""
main() {
var a = new List();
return a.removeLast();
}
""",
KEPT,
"""
main() {
var a = new List(4);
return a[0];
}
""",
REMOVED,
"""
main() {
var a = new List(4);
return a.removeLast();
}
""",
REMOVED,
"""
main(value) {
var a = new List(value);
return a[value];
}
""",
KEPT,
"""
main(value) {
var a = new List(1024);
return a[1023 & value];
}
""",
REMOVED,
"""
main(value) {
var a = new List(1024);
return a[1024 & value];
}
""",
ABOVE_ZERO,
"""
main(value) {
var a = new List();
return a[1];
}
""",
ABOVE_ZERO,
"""
main(value, call) {
var a = new List();
return a[value] + call() + a[value];
}
""",
ONE_ZERO_CHECK,
"""
main(value) {
var a = new List();
return a[1] + a[0];
}
""",
ONE_CHECK,
"""
main() {
var a = new List();
var sum = 0;
for (int i = 0; i <= a.length - 1; i++) {
sum += a[i];
}
return sum;
}
""",
REMOVED,
"""
main() {
var a = new List();
var sum = 0;
for (int i = a.length - 1; i >=0; i--) {
sum += a[i];
}
return sum;
}
""",
REMOVED,
"""
main(value) {
value = value is int ? value as int : 42;
int sum = ~value;
for (int i = 0; i < 42; i++) sum += (value & 4);
var a = new List();
if (value > a.length - 1) return;
if (value < 0) return;
return a[value];
}
""",
REMOVED,
"""
main(value) {
value = value is int ? value as int : 42;
int sum = ~value;
for (int i = 0; i < 42; i++) sum += (value & 4);
var a = new List();
if (value <= a.length - 1) {
if (value >= 0) {
return a[value];
}
}
}
""",
REMOVED,
"""
main(value) {
value = value is int ? value as int : 42;
int sum = ~value;
for (int i = 0; i < 42; i++) sum += (value & 4);
var a = new List();
if (value >= a.length) return;
if (value <= -1) return;
return a[value];
}
""",
REMOVED,
"""
main(value) {
var a = new List(4);
var sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i];
if (sum == 0) i++;
}
return sum;
}
""",
REMOVED,
"""
main(value) {
var a = new List(5);
var sum = 0;
for (int i = a.length - 1; i >= 0; i--) {
sum += a[i];
if (sum == 0) i--;
}
return sum;
}
""",
REMOVED,
"""
main(value) {
var a = new List(6);
var sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i];
if (sum == 0) i--;
}
return sum;
}
""",
BELOW_ZERO_CHECK,
"""
main(value) {
var a = new List(7);
var sum = 0;
for (int i = 0; i < a.length;) {
sum += a[i];
sum == 0 ? i-- : i++;
}
return sum;
}
""",
BELOW_ZERO_CHECK,
"""
main(value) {
var a = new List(7);
var sum = 0;
for (int i = -2; i < a.length; i = 0) {
sum += a[i];
}
return sum;
}
""",
BELOW_ZERO_CHECK,
];
Future expect(String code, int kind) {
return compile(code, check: (String generated) {
switch (kind) {
case REMOVED:
Expect.isFalse(generated.contains('ioore'));
break;
case ABOVE_ZERO:
Expect.isFalse(generated.contains('< 0') || generated.contains('>= 0'));
Expect.isTrue(generated.contains('ioore'));
break;
case BELOW_ZERO_CHECK:
// May generate `!(ix < 0)` or `ix >= 0` depending if `ix` can be NaN
Expect.isTrue(generated.contains('< 0') || generated.contains('>= 0'));
Expect.isFalse(generated.contains('||') || generated.contains('&&'));
Expect.isTrue(generated.contains('ioore'));
break;
case BELOW_LENGTH:
Expect.isFalse(generated.contains('||') || generated.contains('&&'));
Expect.isTrue(generated.contains('ioore'));
break;
case KEPT:
Expect.isTrue(generated.contains('ioore'));
break;
case ONE_CHECK:
RegExp regexp = RegExp('ioore');
Iterator matches = regexp.allMatches(generated).iterator;
checkNumberOfMatches(matches, 1);
break;
case ONE_ZERO_CHECK:
RegExp regexp = RegExp('< 0|>>> 0 !==');
Iterator matches = regexp.allMatches(generated).iterator;
checkNumberOfMatches(matches, 1);
break;
}
});
}
runTests() async {
for (int i = 0; i < TESTS.length; i += 2) {
await expect(TESTS[i], TESTS[i + 1]);
}
}
main() {
asyncTest(() async {
print('--test from kernel------------------------------------------------');
await runTests();
});
}