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
This commit is contained in:
parent
b91a2aec67
commit
b70e764427
3 changed files with 142 additions and 49 deletions
4
Makefile
4
Makefile
|
@ -440,7 +440,7 @@ endif
|
||||||
test: qjs
|
test: qjs
|
||||||
./qjs tests/test_closure.js
|
./qjs tests/test_closure.js
|
||||||
./qjs tests/test_language.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_loop.js
|
||||||
./qjs tests/test_bignum.js
|
./qjs tests/test_bignum.js
|
||||||
./qjs tests/test_std.js
|
./qjs tests/test_std.js
|
||||||
|
@ -461,7 +461,7 @@ endif
|
||||||
ifdef CONFIG_M32
|
ifdef CONFIG_M32
|
||||||
./qjs32 tests/test_closure.js
|
./qjs32 tests/test_closure.js
|
||||||
./qjs32 tests/test_language.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_loop.js
|
||||||
./qjs32 tests/test_bignum.js
|
./qjs32 tests/test_bignum.js
|
||||||
./qjs32 tests/test_std.js
|
./qjs32 tests/test_std.js
|
||||||
|
|
76
quickjs.c
76
quickjs.c
|
@ -49452,39 +49452,63 @@ static double time_clip(double t) {
|
||||||
return NAN;
|
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 */
|
of the operations */
|
||||||
static double set_date_fields(double fields[], int is_local) {
|
static double set_date_fields(double fields[], int is_local) {
|
||||||
int64_t y;
|
double y, m, dt, ym, mn, day, h, s, milli, time, tv;
|
||||||
double days, h, m1;
|
int yi, mi, i;
|
||||||
volatile double d; /* enforce evaluation order */
|
int64_t days;
|
||||||
int i, m, md;
|
volatile double temp; /* enforce evaluation order */
|
||||||
|
|
||||||
m1 = fields[1];
|
/* emulate 21.4.1.15 MakeDay ( year, month, date ) */
|
||||||
m = fmod(m1, 12);
|
y = fields[0];
|
||||||
if (m < 0)
|
m = fields[1];
|
||||||
m += 12;
|
dt = fields[2];
|
||||||
y = (int64_t)(fields[0] + floor(m1 / 12));
|
ym = y + floor(m / 12);
|
||||||
days = days_from_year(y);
|
mn = fmod(m, 12);
|
||||||
|
if (mn < 0)
|
||||||
|
mn += 12;
|
||||||
|
if (ym < -271821 || ym > 275760)
|
||||||
|
return NAN;
|
||||||
|
|
||||||
for(i = 0; i < m; i++) {
|
yi = ym;
|
||||||
md = month_days[i];
|
mi = mn;
|
||||||
|
days = days_from_year(yi);
|
||||||
|
for(i = 0; i < mi; i++) {
|
||||||
|
days += month_days[i];
|
||||||
if (i == 1)
|
if (i == 1)
|
||||||
md += days_in_year(y) - 365;
|
days += days_in_year(yi) - 365;
|
||||||
days += md;
|
|
||||||
}
|
}
|
||||||
days += fields[2] - 1;
|
day = days + dt - 1;
|
||||||
/* made d volatile to ensure order of evaluation as specified in ECMA.
|
|
||||||
* this fixes a test262 error on
|
/* emulate 21.4.1.14 MakeTime ( hour, min, sec, ms ) */
|
||||||
* test262/test/built-ins/Date/UTC/fp-evaluation-order.js
|
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 +
|
time = h * 3600000;
|
||||||
fields[5] * 1000 + fields[6];
|
time += (temp = m * 60000);
|
||||||
d = days * 86400000;
|
time += (temp = s * 1000);
|
||||||
d = d + h;
|
time += milli;
|
||||||
if (is_local)
|
|
||||||
d += getTimezoneOffset(d) * 60000;
|
/* emulate 21.4.1.16 MakeDate ( day, time ) */
|
||||||
return time_clip(d);
|
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,
|
static JSValue get_date_field(JSContext *ctx, JSValueConst this_val,
|
||||||
|
|
|
@ -1,19 +1,51 @@
|
||||||
"use strict";
|
"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 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)
|
if (arguments.length == 1)
|
||||||
expected = true;
|
expected = true;
|
||||||
|
|
||||||
if (actual === expected)
|
if (typeof actual === typeof expected) {
|
||||||
|
if (actual === expected) {
|
||||||
|
if (actual !== 0 || (1 / actual) === (1 / expected))
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
if (typeof actual === 'number') {
|
||||||
|
if (isNaN(actual) && isNaN(expected))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof actual === 'object') {
|
||||||
if (actual !== null && expected !== null
|
if (actual !== null && expected !== null
|
||||||
&& typeof actual == 'object' && typeof expected == 'object'
|
&& actual.constructor === expected.constructor
|
||||||
&& actual.toString() === expected.toString())
|
&& actual.toString() === expected.toString())
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
throw Error("assertion failed: got |" + actual + "|" +
|
}
|
||||||
", expected |" + expected + "|" +
|
// 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 + ")" : ""));
|
(message ? " (" + message + ")" : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,11 +57,16 @@ function assert_throws(expected_error, func)
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
err = true;
|
err = true;
|
||||||
if (!(e instanceof expected_error)) {
|
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) {
|
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(+" 123 ", 123);
|
||||||
assert(+"0b111", 7);
|
assert(+"0b111", 7);
|
||||||
assert(+"0o123", 83);
|
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("0x1234"), 0);
|
||||||
assert(parseFloat("Infinity"), Infinity);
|
assert(parseFloat("Infinity"), Infinity);
|
||||||
assert(parseFloat("-Infinity"), -Infinity);
|
assert(parseFloat("-Infinity"), -Infinity);
|
||||||
|
@ -340,6 +381,11 @@ function test_number()
|
||||||
assert(Number.isNaN(Number("-")));
|
assert(Number.isNaN(Number("-")));
|
||||||
assert(Number.isNaN(Number("\x00a")));
|
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((-25).toExponential(0), "-3e+1");
|
assert((-25).toExponential(0), "-3e+1");
|
||||||
assert((2.5).toPrecision(1), "3");
|
assert((2.5).toPrecision(1), "3");
|
||||||
|
@ -485,21 +531,21 @@ function test_json()
|
||||||
|
|
||||||
function test_date()
|
function test_date()
|
||||||
{
|
{
|
||||||
|
// 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
|
||||||
|
// 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;
|
var d = new Date(1506098258091), a, s;
|
||||||
assert(d.toISOString(), "2017-09-22T16:37:38.091Z");
|
assert(d.toISOString(), "2017-09-22T16:37:38.091Z");
|
||||||
d.setUTCHours(18, 10, 11);
|
d.setUTCHours(18, 10, 11);
|
||||||
assert(d.toISOString(), "2017-09-22T18:10:11.091Z");
|
assert(d.toISOString(), "2017-09-22T18:10:11.091Z");
|
||||||
a = Date.parse(d.toISOString());
|
a = Date.parse(d.toISOString());
|
||||||
assert((new Date(a)).toISOString(), 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.
|
|
||||||
// Hence the fractional part after . should have 3 digits and how
|
|
||||||
// a different number of digits is handled is implementation defined.
|
|
||||||
s = new Date("2020-01-01T01:01:01.1Z").toISOString();
|
s = new Date("2020-01-01T01:01:01.1Z").toISOString();
|
||||||
assert(s, "2020-01-01T01:01:01.100Z");
|
assert(s, "2020-01-01T01:01:01.100Z");
|
||||||
s = new Date("2020-01-01T01:01:01.12Z").toISOString();
|
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();
|
s = new Date("2020-01-01T01:01:01.9999Z").toISOString();
|
||||||
assert(s == "2020-01-01T01:01:02.000Z" || // QuickJS
|
assert(s == "2020-01-01T01:01:02.000Z" || // QuickJS
|
||||||
s == "2020-01-01T01:01:01.999Z"); // nodeJS
|
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()
|
function test_regexp()
|
||||||
|
|
Loading…
Reference in a new issue