]> git.kaiwu.me - quickjs.git/commitdiff
Rewrite `set_date_fields` to match the ECMA specification
authorCharlie Gordon <github@chqrlie.org>
Wed, 21 Feb 2024 20:22:10 +0000 (21:22 +0100)
committerCharlie Gordon <github@chqrlie.org>
Wed, 21 Feb 2024 20:22:10 +0000 (21:22 +0100)
- 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

Makefile
quickjs.c
tests/test_builtin.js

index f16a69a067a49af88d8942cd2bf40093559df1f1..17a0b8e996c347b75dba8af67bdcf099a065de20 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -440,7 +440,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
@@ -461,7 +461,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
index 216b12035422d68bbdfa8c4cad6d24145fa2f996..9024aa82690d744e680e2892953f47c7e8b0b481 100644 (file)
--- a/quickjs.c
+++ b/quickjs.c
@@ -49452,39 +49452,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,
index b51f43872ec625e431683ef35aff8a9016344cf0..f4fece8373ec611a09d19e78c318386f5a3682c8 100644 (file)
@@ -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()