Skip to content

Commit

Permalink
fixup! Normative: Limit time portion of durations to <2⁵³ seconds
Browse files Browse the repository at this point in the history
  • Loading branch information
ptomato committed Nov 29, 2023
1 parent 8b6cbab commit b9f2c58
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 5 deletions.
14 changes: 10 additions & 4 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import OwnPropertyKeys from 'es-abstract/helpers/OwnPropertyKeys.js';
import some from 'es-abstract/helpers/some.js';

import { GetIntrinsic } from './intrinsicclass.mjs';
import { FMAPowerOf10, TruncatingDivModByPowerOf10 } from './math.mjs';
import { CalendarMethodRecord, TimeZoneMethodRecord } from './methodrecord.mjs';
import { TimeDuration } from './timeduration.mjs';
import {
Expand Down Expand Up @@ -3366,17 +3367,17 @@ export function BalanceTimeDuration(norm, largestUnit) {
case 'millisecond':
microseconds = MathTrunc(nanoseconds / 1000);
nanoseconds %= 1000;
milliseconds = MathTrunc(microseconds / 1000) + seconds * 1000;
milliseconds = FMAPowerOf10(seconds, 3, MathTrunc(microseconds / 1000));
microseconds %= 1000;
seconds = 0;
break;
case 'microsecond':
microseconds = MathTrunc(nanoseconds / 1000) + seconds * 1e6;
microseconds = FMAPowerOf10(seconds, 6, MathTrunc(nanoseconds / 1000));
nanoseconds %= 1000;
seconds = 0;
break;
case 'nanosecond':
nanoseconds += seconds * 1e9;
nanoseconds = FMAPowerOf10(seconds, 9, nanoseconds);
seconds = 0;
break;
default:
Expand Down Expand Up @@ -3666,7 +3667,12 @@ export function RejectDuration(y, mon, w, d, h, min, s, ms, µs, ns) {
if (MathAbs(y) >= 2 ** 32 || MathAbs(mon) >= 2 ** 32 || MathAbs(w) >= 2 ** 32) {
throw new RangeError('years, months, and weeks must be < 2³²');
}
if (!NumberIsSafeInteger(d * 86400 + h * 3600 + min * 60 + s + MathTrunc(ms / 1e3 + µs / 1e6 + ns / 1e9))) {
const msResult = TruncatingDivModByPowerOf10(ms, 3);
const µsResult = TruncatingDivModByPowerOf10(µs, 6);
const nsResult = TruncatingDivModByPowerOf10(ns, 9);
const remainderSec = TruncatingDivModByPowerOf10(msResult.mod * 1e6 + µsResult.mod * 1e3 + nsResult.mod, 9).div;
const totalSec = d * 86400 + h * 3600 + min * 60 + s + msResult.div + µsResult.div + nsResult.div + remainderSec;
if (!NumberIsSafeInteger(totalSec)) {
throw new RangeError('total of duration time units cannot exceed 9007199254740991.999999999 s');
}
}
Expand Down
55 changes: 55 additions & 0 deletions polyfill/lib/math.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const MathAbs = Math.abs;
const MathLog10 = Math.log10;
const MathSign = Math.sign;
const MathTrunc = Math.trunc;
const NumberParseInt = Number.parseInt;
const NumberPrototypeToPrecision = Number.prototype.toPrecision;
const StringPrototypePadStart = String.prototype.padStart;
const StringPrototypeRepeat = String.prototype.repeat;
const StringPrototypeSlice = String.prototype.slice;

import Call from 'es-abstract/2022/Call.js';

// Computes trunc(x / 10**p) and x % 10**p, returning { div, mod }, with
// precision loss only once in the quotient, by string manipulation. If the
// quotient and remainder are safe integers, then they are exact. x must be an
// integer. p must be a non-negative integer. Both div and mod have the sign of
// x.
export function TruncatingDivModByPowerOf10(x, p) {
if (x === 0) return { div: x, mod: x }; // preserves signed zero

const sign = MathSign(x);
x = MathAbs(x);

const xDigits = MathTrunc(1 + MathLog10(x));
if (p >= xDigits) return { div: sign * 0, mod: sign * x };
if (p === 0) return { div: sign * x, mod: sign * 0 };

// would perform nearest rounding if x was not an integer:
const xStr = Call(NumberPrototypeToPrecision, x, [xDigits]);
const div = sign * NumberParseInt(Call(StringPrototypeSlice, xStr, [0, xDigits - p]), 10);
const mod = sign * NumberParseInt(Call(StringPrototypeSlice, xStr, [xDigits - p]), 10);

return { div, mod };
}

// Computes x * 10**p + z with precision loss only at the end, by string
// manipulation. If the result is a safe integer, then it is exact. x must be
// an integer. p must be a non-negative integer. z must have the same sign as
// x and be less than 10**p.
export function FMAPowerOf10(x, p, z) {
if (x === 0) return z;

const sign = MathSign(x) || MathSign(z);
x = MathAbs(x);
z = MathAbs(z);

const xStr = Call(NumberPrototypeToPrecision, x, [MathTrunc(1 + MathLog10(x))]);

if (z === 0) return sign * NumberParseInt(xStr + Call(StringPrototypeRepeat, '0', [p]), 10);

const zStr = Call(NumberPrototypeToPrecision, z, [MathTrunc(1 + MathLog10(z))]);

const resStr = xStr + Call(StringPrototypePadStart, zStr, [p, '0']);
return sign * NumberParseInt(resStr, 10);
}
1 change: 0 additions & 1 deletion polyfill/lib/timeduration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export class TimeDuration {
if (n === 0) throw new Error('division by zero');
const { quotient, remainder } = this.totalNs.divmod(n);
const q = quotient.toJSNumber();
if (!NumberIsSafeInteger(q)) throw new Error('assertion failed: quotient too big');
const r = new TimeDuration(remainder);
return { quotient: q, remainder: r };
}
Expand Down
3 changes: 3 additions & 0 deletions polyfill/test/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import './ecmascript.mjs';
// Internal 96-bit integer implementation, not suitable for test262
import './timeduration.mjs';

// Power-of-10 math
import './math.mjs';

Promise.resolve()
.then(() => {
return Demitasse.report(Pretty.reporter);
Expand Down
110 changes: 110 additions & 0 deletions polyfill/test/math.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Demitasse from '@pipobscure/demitasse';
const { describe, it, report } = Demitasse;

import Pretty from '@pipobscure/demitasse-pretty';
const { reporter } = Pretty;

import { strict as assert } from 'assert';
const { deepEqual, equal } = assert;

import {TruncatingDivModByPowerOf10 as div, FMAPowerOf10 as fma} from '../lib/math.mjs';

Check failure on line 10 in polyfill/test/math.mjs

View workflow job for this annotation

GitHub Actions / lint

A space is required after '{'

Check failure on line 10 in polyfill/test/math.mjs

View workflow job for this annotation

GitHub Actions / lint

Replace `TruncatingDivModByPowerOf10·as·div,·FMAPowerOf10·as·fma` with `·TruncatingDivModByPowerOf10·as·div,·FMAPowerOf10·as·fma·`

Check failure on line 10 in polyfill/test/math.mjs

View workflow job for this annotation

GitHub Actions / lint

A space is required before '}'

describe('Math', () => {
describe('TruncatingDivModByPowerOf10', () => {
it('12345/10**0 = 12345, 0', () => deepEqual(div(12345, 0), { div: 12345, mod: 0 }));
it('12345/10**1 = 1234, 5', () => deepEqual(div(12345, 1), { div: 1234, mod: 5 }));
it('12345/10**2 = 123, 45', () => deepEqual(div(12345, 2), { div: 123, mod: 45 }));
it('12345/10**3 = 12, 345', () => deepEqual(div(12345, 3), { div: 12, mod: 345 }));
it('12345/10**4 = 1, 2345', () => deepEqual(div(12345, 4), { div: 1, mod: 2345 }));
it('12345/10**5 = 0, 12345', () => deepEqual(div(12345, 5), { div: 0, mod: 12345 }));
it('12345/10**6 = 0, 12345', () => deepEqual(div(12345, 6), { div: 0, mod: 12345 }));

it('-12345/10**0 = -12345, -0', () => deepEqual(div(-12345, 0), { div: -12345, mod: -0 }));
it('-12345/10**1 = -1234, -5', () => deepEqual(div(-12345, 1), { div: -1234, mod: -5 }));
it('-12345/10**2 = -123, -45', () => deepEqual(div(-12345, 2), { div: -123, mod: -45 }));
it('-12345/10**3 = -12, -345', () => deepEqual(div(-12345, 3), { div: -12, mod: -345 }));
it('-12345/10**4 = -1, -2345', () => deepEqual(div(-12345, 4), { div: -1, mod: -2345 }));
it('-12345/10**5 = -0, -12345', () => deepEqual(div(-12345, 5), { div: -0, mod: -12345 }));
it('-12345/10**6 = -0, -12345', () => deepEqual(div(-12345, 6), { div: -0, mod: -12345 }));

it('0/10**27 = 0, 0', () => deepEqual(div(0, 27), { div: 0, mod: 0 }));
it('-0/10**27 = -0, -0', () => deepEqual(div(-0, 27), { div: -0, mod: -0 }));

it('1001/10**3 = 1, 1', () => deepEqual(div(1001, 3), { div: 1, mod: 1 }));
it('-1001/10**3 = -1, -1', () => deepEqual(div(-1001, 3), { div: -1, mod: -1 }));

it('4019125567429664768/10**3 = 4019125567429664, 768', () =>
deepEqual(div(4019125567429664768, 3), { div: 4019125567429664, mod: 768 }));
it('-4019125567429664768/10**3 = -4019125567429664, -768', () =>
deepEqual(div(-4019125567429664768, 3), { div: -4019125567429664, mod: -768 }));
it('3294477463410151260160/10**6 = 3294477463410151, 260160', () =>
deepEqual(div(3294477463410151260160, 6), { div: 3294477463410151, mod: 260160 }));
it('-3294477463410151260160/10**6 = -3294477463410151, -260160', () =>
deepEqual(div(-3294477463410151260160, 6), { div: -3294477463410151, mod: -260160 }));
it('7770017954545649059889152/10**9 = 7770017954545649, 59889152', () =>
deepEqual(div(7770017954545649059889152, 9), { div: 7770017954545649, mod: 59889152 }));
it('-7770017954545649059889152/-10**9 = -7770017954545649, -59889152', () =>
deepEqual(div(-7770017954545649059889152, 9), { div: -7770017954545649, mod: -59889152 }));

// Largest/smallest representable float that will result in a safe quotient,
// for each of the divisors 10**3, 10**6, 10**9
it('9007199254740990976/10**3 = MAX_SAFE_INTEGER-1, 976', () =>
deepEqual(div(9007199254740990976, 3), { div: Number.MAX_SAFE_INTEGER - 1, mod: 976 }));
it('-9007199254740990976/10**3 = -MAX_SAFE_INTEGER+1, -976', () =>
deepEqual(div(-9007199254740990976, 3), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -976 }));
it('9007199254740990951424/10**6 = MAX_SAFE_INTEGER-1, 951424', () =>
deepEqual(div(9007199254740990951424, 6), { div: Number.MAX_SAFE_INTEGER - 1, mod: 951424 }));
it('-9007199254740990951424/10**6 = -MAX_SAFE_INTEGER+1, -951424', () =>
deepEqual(div(-9007199254740990951424, 6), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -951424 }));
it('9007199254740990926258176/10**9 = MAX_SAFE_INTEGER-1, 926258176', () =>
deepEqual(div(9007199254740990926258176, 9), { div: Number.MAX_SAFE_INTEGER - 1, mod: 926258176 }));
it('-9007199254740990926258176/10**9 = -MAX_SAFE_INTEGER+1, -926258176', () =>
deepEqual(div(-9007199254740990926258176, 9), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -926258176 }));
});

describe('FMAPowerOf10', () => {
it('0*10**0+0 = 0', () => equal(fma(0, 0, 0), 0));
it('-0*10**0-0 = -0', () => equal(fma(-0, 0, -0), -0));
it('1*10**0+0 = 1', () => equal(fma(1, 0, 0), 1));
it('-1*10**0+0 = -1', () => equal(fma(-1, 0, 0), -1));
it('0*10**50+1234 = 1234', () => equal(fma(0, 50, 1234), 1234));
it('-0*10**50-1234 = -1234', () => equal(fma(-0, 50, -1234), -1234));
it('1234*10**12+0', () => equal(fma(1234, 12, 0), 1234000000000000));
it('-1234*10**12-0', () => equal(fma(-1234, 12, -0), -1234000000000000));

it('2*10**2+45 = 245', () => equal(fma(2, 2, 45), 245));
it('2*10**3+45 = 2045', () => equal(fma(2, 3, 45), 2045));
it('2*10**4+45 = 20045', () => equal(fma(2, 4, 45), 20045));
it('2*10**5+45 = 200045', () => equal(fma(2, 5, 45), 200045));
it('2*10**6+45 = 2000045', () => equal(fma(2, 6, 45), 2000045));

it('-2*10**2-45 = -245', () => equal(fma(-2, 2, -45), -245));
it('-2*10**3-45 = -2045', () => equal(fma(-2, 3, -45), -2045));
it('-2*10**4-45 = -20045', () => equal(fma(-2, 4, -45), -20045));
it('-2*10**5-45 = -200045', () => equal(fma(-2, 5, -45), -200045));
it('-2*10**6-45 = -2000045', () => equal(fma(-2, 6, -45), -2000045));

it('8692288669465520*10**9+321414345 = 8692288669465520321414345, rounded to 8692288669465520839327744', () =>
equal(fma(8692288669465520, 9, 321414345), 8692288669465520839327744));
it('-8692288669465520*10**9-321414345 = -8692288669465520321414345, rounded to -8692288669465520839327744', () =>
equal(fma(-8692288669465520, 9, -321414345), -8692288669465520839327744));

it('MAX_SAFE_INTEGER*10**3+999 rounded to 9007199254740992000', () =>
equal(fma(Number.MAX_SAFE_INTEGER, 3, 999), 9007199254740992000));
it('-MAX_SAFE_INTEGER*10**3-999 rounded to -9007199254740992000', () =>
equal(fma(-Number.MAX_SAFE_INTEGER, 3, -999), -9007199254740992000));
it('MAX_SAFE_INTEGER*10**6+999999 rounded to 9007199254740992000000', () =>
equal(fma(Number.MAX_SAFE_INTEGER, 6, 999999), 9007199254740992000000));
it('-MAX_SAFE_INTEGER*10**6-999999 rounded to -9007199254740992000000', () =>
equal(fma(-Number.MAX_SAFE_INTEGER, 6, -999999), -9007199254740992000000));
it('MAX_SAFE_INTEGER*10**3+999 rounded to 9007199254740992000', () =>
equal(fma(Number.MAX_SAFE_INTEGER, 9, 999999999), 9007199254740992000000000));
it('-MAX_SAFE_INTEGER*10**3-999 rounded to -9007199254740992000', () =>
equal(fma(-Number.MAX_SAFE_INTEGER, 9, -999999999), -9007199254740992000000000));
});
});

import { normalize } from 'path';
if (normalize(import.meta.url.slice(8)) === normalize(process.argv[1])) {
report(reporter).then((failed) => process.exit(failed ? 1 : 0));
}
2 changes: 2 additions & 0 deletions spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ <h1>
1. If abs(_months_) &ge; 2<sup>32</sup>, return *false*.
1. If abs(_weeks_) &ge; 2<sup>32</sup>, return *false*.
1. Let _normalizedSeconds_ be _days_ &times; 86,400 + _hours_ &times; 3600 + _minutes_ &times; 60 + _seconds_ + _milliseconds_ &times; 10<sup>-3</sup> + _microseconds_ &times; 10<sup>-6</sup> + _nanoseconds_ &times; 10<sup>-9</sup>.
1. NOTE: The above step cannot be implemented directly using floating-point arithmetic. Multiplying by 10<sup>-3</sup>, 10<sup>-6</sup>, and 10<sup>-9</sup> respectively may be imprecise when _milliseconds_, _microseconds_, or _nanoseconds_ is an unsafe integer. This multiplication can be implemented in C++ with an implementation of `std::remquo()` with sufficient bits in the quotient. String manipulation will also give an exact result, since the multiplication is by a power of 10.
1. If abs(_normalizedSeconds_) &ge; 2<sup>53</sup>, return *false*.
1. Return *true*.
</emu-alg>
Expand Down Expand Up @@ -1666,6 +1667,7 @@ <h1>
1. Set _nanoseconds_ to _nanoseconds_ modulo 1000.
1. Else,
1. Assert: _largestUnit_ is *"nanosecond"*.
1. NOTE: When _largestUnit_ is *"millisecond"*, *"microsecond"*, or *"nanosecond"*, _milliseconds_, _microseconds_, or _nanoseconds_ may be an unsafe integer. In this case, care must be taken when implementing the calculation using floating point arithmetic. It can be implemented in C++ using `std::fma()`. String manipulation will also give an exact result, since the multiplication is by a power of 10.
1. Return ! CreateTimeDurationRecord(_days_ &times; _sign_, _hours_ &times; _sign_, _minutes_ &times; _sign_, _seconds_ &times; _sign_, _milliseconds_ &times; _sign_, _microseconds_ &times; _sign_, _nanoseconds_ &times; _sign_).
</emu-alg>
</emu-clause>
Expand Down

0 comments on commit b9f2c58

Please sign in to comment.