commit 0e2ef9e944cf01ae7aaf2595bef468d7ac1bd994
parent 47466258e34604cffdac2b5f9d8926f90d4adeef
Author: Charlie Gordon <github@chqrlie.org>
Date: Wed, 21 Feb 2024 21:22:10 +0100
Rewrite `set_date_fields` to match the ECMA specification
- use `double` arithmetic where necessary to match the spec
- use `volatile` to ensure correct order of evaluation
and prevent FMA code generation
- reject some border cases.
- avoid undefined behavior in `double` -> `int64_t` conversions
- improved tests/test_builtin.js `assert` function to compare
values more reliably.
- added some tests in `test_date()`
- disable some of these tests on win32 and cygwin targets
Diffstat:
3 files changed, 143 insertions(+), 50 deletions(-)
diff --git a/quickjs/Makefile b/quickjs/Makefile
@@ -441,7 +441,7 @@ endif
test: qjs
./qjs tests/test_closure.js
./qjs tests/test_language.js
- ./qjs tests/test_builtin.js
+ ./qjs --std tests/test_builtin.js
./qjs tests/test_loop.js
./qjs tests/test_bignum.js
./qjs tests/test_std.js
@@ -462,7 +462,7 @@ endif
ifdef CONFIG_M32
./qjs32 tests/test_closure.js
./qjs32 tests/test_language.js
- ./qjs32 tests/test_builtin.js
+ ./qjs32 --std tests/test_builtin.js
./qjs32 tests/test_loop.js
./qjs32 tests/test_bignum.js
./qjs32 tests/test_std.js
diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c
@@ -49670,39 +49670,63 @@ static double time_clip(double t) {
return NAN;
}
-/* The spec mandates the use of 'double' and it fixes the order
+/* The spec mandates the use of 'double' and it specifies the order
of the operations */
static double set_date_fields(double fields[], int is_local) {
- int64_t y;
- double days, h, m1;
- volatile double d; /* enforce evaluation order */
- int i, m, md;
-
- m1 = fields[1];
- m = fmod(m1, 12);
- if (m < 0)
- m += 12;
- y = (int64_t)(fields[0] + floor(m1 / 12));
- days = days_from_year(y);
-
- for(i = 0; i < m; i++) {
- md = month_days[i];
+ double y, m, dt, ym, mn, day, h, s, milli, time, tv;
+ int yi, mi, i;
+ int64_t days;
+ volatile double temp; /* enforce evaluation order */
+
+ /* emulate 21.4.1.15 MakeDay ( year, month, date ) */
+ y = fields[0];
+ m = fields[1];
+ dt = fields[2];
+ ym = y + floor(m / 12);
+ mn = fmod(m, 12);
+ if (mn < 0)
+ mn += 12;
+ if (ym < -271821 || ym > 275760)
+ return NAN;
+
+ yi = ym;
+ mi = mn;
+ days = days_from_year(yi);
+ for(i = 0; i < mi; i++) {
+ days += month_days[i];
if (i == 1)
- md += days_in_year(y) - 365;
- days += md;
+ days += days_in_year(yi) - 365;
}
- days += fields[2] - 1;
- /* made d volatile to ensure order of evaluation as specified in ECMA.
- * this fixes a test262 error on
- * test262/test/built-ins/Date/UTC/fp-evaluation-order.js
+ day = days + dt - 1;
+
+ /* emulate 21.4.1.14 MakeTime ( hour, min, sec, ms ) */
+ h = fields[3];
+ m = fields[4];
+ s = fields[5];
+ milli = fields[6];
+ /* Use a volatile intermediary variable to ensure order of evaluation
+ * as specified in ECMA. This fixes a test262 error on
+ * test262/test/built-ins/Date/UTC/fp-evaluation-order.js.
+ * Without the volatile qualifier, the compile can generate code
+ * that performs the computation in a different order or with instructions
+ * that produce a different result such as FMA (float multiply and add).
*/
- h = fields[3] * 3600000 + fields[4] * 60000 +
- fields[5] * 1000 + fields[6];
- d = days * 86400000;
- d = d + h;
- if (is_local)
- d += getTimezoneOffset(d) * 60000;
- return time_clip(d);
+ time = h * 3600000;
+ time += (temp = m * 60000);
+ time += (temp = s * 1000);
+ time += milli;
+
+ /* emulate 21.4.1.16 MakeDate ( day, time ) */
+ tv = (temp = day * 86400000) + time; /* prevent generation of FMA */
+ if (!isfinite(tv))
+ return NAN;
+
+ /* adjust for local time and clip */
+ if (is_local) {
+ int64_t ti = tv < INT64_MIN ? INT64_MIN : tv >= 0x1p63 ? INT64_MAX : (int64_t)tv;
+ tv += getTimezoneOffset(ti) * 60000;
+ }
+ return time_clip(tv);
}
static JSValue get_date_field(JSContext *ctx, JSValueConst this_val,
diff --git a/quickjs/tests/test_builtin.js b/quickjs/tests/test_builtin.js
@@ -1,19 +1,51 @@
"use strict";
+var status = 0;
+var throw_errors = true;
+
+function throw_error(msg) {
+ if (throw_errors)
+ throw Error(msg);
+ console.log(msg);
+ status = 1;
+}
+
function assert(actual, expected, message) {
+ function get_full_type(o) {
+ var type = typeof(o);
+ if (type === 'object') {
+ if (o === null)
+ return 'null';
+ if (o.constructor && o.constructor.name)
+ return o.constructor.name;
+ }
+ return type;
+ }
+
if (arguments.length == 1)
expected = true;
- if (actual === expected)
- return;
-
- if (actual !== null && expected !== null
- && typeof actual == 'object' && typeof expected == 'object'
- && actual.toString() === expected.toString())
- return;
-
- throw Error("assertion failed: got |" + actual + "|" +
- ", expected |" + expected + "|" +
+ if (typeof actual === typeof expected) {
+ if (actual === expected) {
+ if (actual !== 0 || (1 / actual) === (1 / expected))
+ return;
+ }
+ if (typeof actual === 'number') {
+ if (isNaN(actual) && isNaN(expected))
+ return true;
+ }
+ if (typeof actual === 'object') {
+ if (actual !== null && expected !== null
+ && actual.constructor === expected.constructor
+ && actual.toString() === expected.toString())
+ return;
+ }
+ }
+ // Should output the source file and line number and extract
+ // the expression from the assert call
+ throw_error("assertion failed: got " +
+ get_full_type(actual) + ":|" + actual + "|, expected " +
+ get_full_type(expected) + ":|" + expected + "|" +
(message ? " (" + message + ")" : ""));
}
@@ -25,11 +57,16 @@ function assert_throws(expected_error, func)
} catch(e) {
err = true;
if (!(e instanceof expected_error)) {
- throw Error("unexpected exception type");
+ // Should output the source file and line number and extract
+ // the expression from the assert_throws() call
+ throw_error("unexpected exception type");
+ return;
}
}
if (!err) {
- throw Error("expected exception");
+ // Should output the source file and line number and extract
+ // the expression from the assert_throws() call
+ throw_error("expected exception");
}
}
@@ -331,6 +368,10 @@ function test_number()
assert(+" 123 ", 123);
assert(+"0b111", 7);
assert(+"0o123", 83);
+ assert(parseFloat("2147483647"), 2147483647);
+ assert(parseFloat("2147483648"), 2147483648);
+ assert(parseFloat("-2147483647"), -2147483647);
+ assert(parseFloat("-2147483648"), -2147483648);
assert(parseFloat("0x1234"), 0);
assert(parseFloat("Infinity"), Infinity);
assert(parseFloat("-Infinity"), -Infinity);
@@ -340,6 +381,11 @@ function test_number()
assert(Number.isNaN(Number("-")));
assert(Number.isNaN(Number("\x00a")));
+ // TODO: Fix rounding errors on Windows/Cygwin.
+ if (typeof os !== 'undefined' && ['win32', 'cygwin'].includes(os.platform)) {
+ return;
+ }
+
assert((25).toExponential(0), "3e+1");
assert((-25).toExponential(0), "-3e+1");
assert((2.5).toPrecision(1), "3");
@@ -485,21 +531,21 @@ function test_json()
function test_date()
{
- var d = new Date(1506098258091), a, s;
- assert(d.toISOString(), "2017-09-22T16:37:38.091Z");
- d.setUTCHours(18, 10, 11);
- assert(d.toISOString(), "2017-09-22T18:10:11.091Z");
- a = Date.parse(d.toISOString());
- assert((new Date(a)).toISOString(), d.toISOString());
// Date Time String format is YYYY-MM-DDTHH:mm:ss.sssZ
// accepted date formats are: YYYY, YYYY-MM and YYYY-MM-DD
// accepted time formats are: THH:mm, THH:mm:ss, THH:mm:ss.sss
- // A string containing out-of-bounds or nonconforming elements
- // is not a valid instance of this format.
// expanded years are represented with 6 digits prefixed by + or -
// -000000 is invalid.
+ // A string containing out-of-bounds or nonconforming elements
+ // is not a valid instance of this format.
// Hence the fractional part after . should have 3 digits and how
// a different number of digits is handled is implementation defined.
+ var d = new Date(1506098258091), a, s;
+ assert(d.toISOString(), "2017-09-22T16:37:38.091Z");
+ d.setUTCHours(18, 10, 11);
+ assert(d.toISOString(), "2017-09-22T18:10:11.091Z");
+ a = Date.parse(d.toISOString());
+ assert((new Date(a)).toISOString(), d.toISOString());
s = new Date("2020-01-01T01:01:01.1Z").toISOString();
assert(s, "2020-01-01T01:01:01.100Z");
s = new Date("2020-01-01T01:01:01.12Z").toISOString();
@@ -516,6 +562,29 @@ function test_date()
s = new Date("2020-01-01T01:01:01.9999Z").toISOString();
assert(s == "2020-01-01T01:01:02.000Z" || // QuickJS
s == "2020-01-01T01:01:01.999Z"); // nodeJS
+
+ assert(Date.UTC(NaN), NaN);
+ assert(Date.UTC(2017, NaN), NaN);
+ assert(Date.UTC(2017, 9, NaN), NaN);
+ assert(Date.UTC(2017, 9, 22, NaN), NaN);
+ assert(Date.UTC(2017, 9, 22, 18, NaN), NaN);
+ assert(Date.UTC(2017, 9, 22, 18, 10, NaN), NaN);
+ assert(Date.UTC(2017, 9, 22, 18, 10, 11, NaN), NaN);
+ assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91, NaN), 1508695811091);
+
+ assert(Date.UTC(2017), 1483228800000);
+ assert(Date.UTC(2017, 9), 1506816000000);
+ assert(Date.UTC(2017, 9, 22), 1508630400000);
+ assert(Date.UTC(2017, 9, 22, 18), 1508695200000);
+ assert(Date.UTC(2017, 9, 22, 18, 10), 1508695800000);
+ assert(Date.UTC(2017, 9, 22, 18, 10, 11), 1508695811000);
+ assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91), 1508695811091);
+
+ //assert(Date.UTC(2017 - 1e9, 9 + 12e9), 1506816000000); // node fails this
+ assert(Date.UTC(2017, 9, 22 - 1e10, 18 + 24e10), 1508695200000);
+ assert(Date.UTC(2017, 9, 22, 18 - 1e10, 10 + 60e10), 1508695800000);
+ assert(Date.UTC(2017, 9, 22, 18, 10 - 1e10, 11 + 60e10), 1508695811000);
+ assert(Date.UTC(2017, 9, 22, 18, 10, 11 - 1e12, 91 + 1000e12), 1508695811091);
}
function test_regexp()