Skip to content

Commit d610eb6

Browse files
ExE-BossTimothyGu
andcommitted
Use BigInt for 64‑bit integer conversion
Co-authored-by: Timothy Gu <[email protected]>
1 parent e57c072 commit d610eb6

File tree

3 files changed

+179
-107
lines changed

3 files changed

+179
-107
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ Derived types, such as nullable types, promise types, sequences, records, etc. a
7070

7171
### A note on the `long long` types
7272

73-
The `long long` and `unsigned long long` Web IDL types can hold values that cannot be stored in JavaScript numbers, so the conversion is imperfect. For example, converting the JavaScript number `18446744073709552000` to a Web IDL `long long` is supposed to produce the Web IDL value `-18446744073709551232`. Since we are representing our Web IDL values in JavaScript, we can't represent `-18446744073709551232`, so we instead the best we could do is `-18446744073709552000` as the output.
73+
The `long long` and `unsigned long long` Web IDL types can hold values that cannot be stored in JavaScript numbers. Conversions are still accurate as we make use of BigInt in the conversion process, but in the case of `unsigned long long` we simply cannot represent some possible output values in JavaScript. For example, converting the JavaScript number `-1` to a Web IDL `unsigned long long` is supposed to produce the Web IDL value `18446744073709551615`. Since we are representing our Web IDL values in JavaScript, we can't represent `18446744073709551615`, so we instead the best we could do is `18446744073709551616` as the output.
7474

75-
This library actually doesn't even get that far. Producing those results would require doing accurate modular arithmetic on 64-bit intermediate values, but JavaScript does not make this easy. We could pull in a big-integer library as a dependency, but in lieu of that, we for now have decided to just produce inaccurate results if you pass in numbers that are not strictly between `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`.
75+
To mitigate this, we could return the raw BigInt value from the conversion function, but right now it is not implemented. If your use case requires such precision, [file an issue](https://github.com/jsdom/webidl-conversions/issues/new).
76+
77+
On the other hand, `long long` conversion is always accurate, since the input value can never be more precise than the output value.
7678

7779
## Background
7880

lib/index.js

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ function createIntegerConversion(bitLength, typeOpts) {
9494
let lowerBound;
9595
let upperBound;
9696
if (bitLength === 64) {
97-
upperBound = Math.pow(2, 53) - 1;
98-
lowerBound = !isSigned ? 0 : -Math.pow(2, 53) + 1;
97+
upperBound = Number.MAX_SAFE_INTEGER;
98+
lowerBound = !isSigned ? 0 : Number.MIN_SAFE_INTEGER;
9999
} else if (!isSigned) {
100100
lowerBound = 0;
101101
upperBound = Math.pow(2, bitLength) - 1;
@@ -113,7 +113,7 @@ function createIntegerConversion(bitLength, typeOpts) {
113113
}
114114

115115
let x = toNumber(V, opts);
116-
x = censorNegativeZero(x); // Spec discussion ongoing: https://github.com/heycam/webidl/issues/306
116+
x = censorNegativeZero(x);
117117

118118
if (opts.enforceRange) {
119119
if (!Number.isFinite(x)) {
@@ -156,6 +156,50 @@ function createIntegerConversion(bitLength, typeOpts) {
156156
};
157157
}
158158

159+
function createLongLongConversion(bitLength, { unsigned }) {
160+
const upperBound = Number.MAX_SAFE_INTEGER;
161+
const lowerBound = unsigned ? 0 : Number.MIN_SAFE_INTEGER;
162+
const asBigIntN = unsigned ? BigInt.asUintN : BigInt.asIntN;
163+
164+
return (V, opts = {}) => {
165+
if (opts === undefined) {
166+
opts = {};
167+
}
168+
169+
let x = toNumber(V, opts);
170+
x = censorNegativeZero(x);
171+
172+
if (opts.enforceRange) {
173+
if (!Number.isFinite(x)) {
174+
throw makeException(TypeError, "is not a finite number", opts);
175+
}
176+
177+
x = integerPart(x);
178+
179+
if (x < lowerBound || x > upperBound) {
180+
throw makeException(TypeError,
181+
`is outside the accepted range of ${lowerBound} to ${upperBound}, inclusive`, opts);
182+
}
183+
184+
return x;
185+
}
186+
187+
if (!Number.isNaN(x) && opts.clamp) {
188+
x = Math.min(Math.max(x, lowerBound), upperBound);
189+
x = evenRound(x);
190+
return x;
191+
}
192+
193+
if (!Number.isFinite(x) || x === 0) {
194+
return 0;
195+
}
196+
197+
let xBigInt = BigInt(integerPart(x));
198+
xBigInt = asBigIntN(bitLength, xBigInt);
199+
return Number(xBigInt);
200+
};
201+
}
202+
159203
exports.any = V => {
160204
return V;
161205
};
@@ -177,8 +221,8 @@ exports["unsigned short"] = createIntegerConversion(16, { unsigned: true });
177221
exports.long = createIntegerConversion(32, { unsigned: false });
178222
exports["unsigned long"] = createIntegerConversion(32, { unsigned: true });
179223

180-
exports["long long"] = createIntegerConversion(64, { unsigned: false });
181-
exports["unsigned long long"] = createIntegerConversion(64, { unsigned: true });
224+
exports["long long"] = createLongLongConversion(64, { unsigned: false });
225+
exports["unsigned long long"] = createLongLongConversion(64, { unsigned: true });
182226

183227
exports.double = (V, opts) => {
184228
const x = toNumber(V, opts);

test/integer-types.js

Lines changed: 126 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,24 @@ const assert = require("assert");
44
const conversions = require("..");
55
const assertThrows = require("./assertThrows");
66

7-
function assertIs(actual, expected, message) {
7+
// For extraordinarily large (in magnitude) numbers, we can't rely on toString() to give the most accurate output. See
8+
// the note at https://tc39.es/ecma262/#sec-number.prototype.tofixed:
9+
//
10+
// > The output of toFixed may be more precise than toString for some values because toString only prints enough
11+
// > significant digits to distinguish the number from adjacent number values. For example,
12+
// >
13+
// > (1000000000000000128).toString() returns "1000000000000000100", while
14+
// > (1000000000000000128).toFixed(0) returns "1000000000000000128".
15+
function stringifyNumber(num) {
16+
if (Number.isFinite(num) && Number.isInteger(num) && !Number.isSafeInteger(num)) {
17+
return num.toFixed(0);
18+
}
19+
return String(num);
20+
}
21+
22+
function assertIs(actual, expected) {
823
if (!Object.is(actual, expected)) {
9-
assert.fail(actual, expected, message, "is", assertIs);
24+
assert.fail(`Input ${stringifyNumber(actual)} expected to be ${stringifyNumber(expected)}`);
1025
}
1126
}
1227

@@ -96,16 +111,13 @@ function commonTestNonFinite(sut) {
96111
function generateTests(sut, testCases, options, extraLabel) {
97112
extraLabel = extraLabel === undefined ? "" : " " + extraLabel;
98113

99-
for (const testCase of testCases) {
100-
const input = testCase[0];
101-
const expected = testCase[1];
102-
114+
for (const [input, expected] of testCases) {
103115
if (expected === TypeError) {
104-
it("should throw for " + input + extraLabel, () => {
116+
it(`should throw for ${stringifyNumber(input)}${extraLabel}`, () => {
105117
assertThrows(sut, [input, options], TypeError);
106118
});
107119
} else {
108-
it(`should return ${expected} for ${input}${extraLabel}`, () => {
120+
it(`should return ${stringifyNumber(expected)} for ${stringifyNumber(input)}${extraLabel}`, () => {
109121
assertIs(sut(input, options), expected);
110122
});
111123
}
@@ -422,55 +434,62 @@ describe("WebIDL long long type", () => {
422434
commonTestNonFinite(sut);
423435

424436
generateTests(sut, [
425-
[4294967296, 4294967296],
426-
[9007199254740991, 9007199254740991],
427-
[9007199254740992, 9007199254740992],
428-
[9007199254740993, 9007199254740992], // This is just JavaScript; no Web IDL special behavior involved
429-
[9007199254740994, 9007199254740994],
430-
[4611686018427388000, 4611686018427388000],
431-
[9223372036854776000, -9223372036854776000],
432-
[-4294967295, -4294967295],
433-
[-9007199254740991, -9007199254740991]
437+
[4294967296, 4294967296], // 2**32
438+
[9007199254740991, 9007199254740991], // 2**53 - 1 = Number.MAX_SAFE_INTEGER
439+
[9007199254740992, 9007199254740992], // 2**53
440+
[9007199254740993, 9007199254740992], // This is just JavaScript, as 9007199254740993 === 9007199254740992
441+
[9007199254740994, 9007199254740994], // 2**53 + 2
442+
[4611686018427387904, 4611686018427387904], // 2**62
443+
[9223372036854775808, -9223372036854775808], // 2**63
444+
[9223372036854777856, -9223372036854773760], // 2**63 + 2**11
445+
[18446744073709551616, 0], // 2**64
446+
[18446744073709555712, 4096], // 2**64 + 2**12
447+
[-4294967296, -4294967296], // -2**32
448+
[-9007199254740991, -9007199254740991], // -(2**53 - 1) = Number.MIN_SAFE_INTEGER
449+
[-9007199254740992, -9007199254740992], // -(2**53)
450+
[-18446744073709551616, 0], // -(2**64)
451+
[-18446744073709555712, -4096] // -(2**64 + 2**12)
434452
]);
435453

436-
generateTests(sut, [
437-
[4294967296, 4294967296],
438-
[9007199254740991, 9007199254740991],
439-
[9007199254740992, 9007199254740991],
440-
[9007199254740993, 9007199254740991],
441-
[9007199254740994, 9007199254740991],
442-
[4611686018427388000, 9007199254740991],
443-
[9223372036854776000, 9007199254740991],
444-
[18446744073709552000, 9007199254740991],
445-
[-4294967295, -4294967295],
446-
[-9007199254740991, -9007199254740991],
447-
[-9007199254740992, -9007199254740991],
448-
[-18446744073709552000, -9007199254740991]
449-
], { clamp: true }, "with [Clamp]");
450-
451-
generateTests(sut, [
452-
[4294967296, 4294967296],
453-
[9007199254740991, 9007199254740991],
454-
[9007199254740992, TypeError],
455-
[9007199254740993, TypeError],
456-
[9007199254740994, TypeError],
457-
[4611686018427388000, TypeError],
458-
[9223372036854776000, TypeError],
459-
[18446744073709552000, TypeError],
460-
[-4294967295, -4294967295],
461-
[-9007199254740991, -9007199254740991],
462-
[-9007199254740992, TypeError],
463-
[-18446744073709552000, TypeError]
464-
], { enforceRange: true }, "with [EnforceRange]");
465-
});
466-
467-
describe.skip("WebIDL long long test cases hard to pass within JavaScript", () => {
468-
// See the README for more details.
469-
const sut = conversions["long long"];
454+
describe("[Clamp]", () => {
455+
generateTests(sut, [
456+
[4294967296, 4294967296], // 2**32
457+
[9007199254740991, Number.MAX_SAFE_INTEGER], // 2**53 - 1 = Number.MAX_SAFE_INTEGER
458+
[9007199254740992, Number.MAX_SAFE_INTEGER], // 2**53
459+
[9007199254740993, Number.MAX_SAFE_INTEGER], // 2**53 + 1
460+
[9007199254740994, Number.MAX_SAFE_INTEGER], // 2**53 + 2
461+
[4611686018427387904, Number.MAX_SAFE_INTEGER], // 2**62
462+
[9223372036854775808, Number.MAX_SAFE_INTEGER], // 2**63
463+
[9223372036854777856, Number.MAX_SAFE_INTEGER], // 2**63 + 2**11
464+
[18446744073709551616, Number.MAX_SAFE_INTEGER], // 2**64
465+
[18446744073709555712, Number.MAX_SAFE_INTEGER], // 2**64 + 2**12
466+
[-4294967296, -4294967296], // -2**32
467+
[-9007199254740991, Number.MIN_SAFE_INTEGER], // -(2**53 - 1) = Number.MIN_SAFE_INTEGER
468+
[-9007199254740992, Number.MIN_SAFE_INTEGER], // -(2**53)
469+
[-18446744073709551616, Number.MIN_SAFE_INTEGER], // -(2**64)
470+
[-18446744073709555712, Number.MIN_SAFE_INTEGER] // -(2**64 + 2**12)
471+
], { clamp: true }, "with [Clamp]");
472+
});
470473

471-
generateTests(sut, [
472-
[18446744073709552000, -18446744073709552000]
473-
]);
474+
describe("[EnforceRange]", () => {
475+
generateTests(sut, [
476+
[4294967296, 4294967296], // 2**32
477+
[9007199254740991, Number.MAX_SAFE_INTEGER], // 2**53 - 1 = Number.MAX_SAFE_INTEGER
478+
[9007199254740992, TypeError], // 2**53
479+
[9007199254740993, TypeError], // 2**53 + 1
480+
[9007199254740994, TypeError], // 2**53 + 2
481+
[4611686018427387904, TypeError], // 2**62
482+
[9223372036854775808, TypeError], // 2**63
483+
[9223372036854777856, TypeError], // 2**63 + 2**11
484+
[18446744073709551616, TypeError], // 2**64
485+
[18446744073709555712, TypeError], // 2**64 + 2**12
486+
[-4294967296, -4294967296], // -2**32
487+
[-9007199254740991, Number.MIN_SAFE_INTEGER], // -(2**53 - 1) = Number.MIN_SAFE_INTEGER
488+
[-9007199254740992, TypeError], // -(2**53)
489+
[-18446744073709551616, TypeError], // -(2**64)
490+
[-18446744073709555712, TypeError] // -(2**64 + 2**12)
491+
], { enforceRange: true }, "with [EnforceRange]");
492+
});
474493
});
475494

476495
describe("WebIDL unsigned long long type", () => {
@@ -480,53 +499,60 @@ describe("WebIDL unsigned long long type", () => {
480499
commonTestNonFinite(sut);
481500

482501
generateTests(sut, [
483-
[4294967296, 4294967296],
484-
[9007199254740991, 9007199254740991],
485-
[9007199254740992, 9007199254740992],
486-
[9007199254740993, 9007199254740992], // This is just JavaScript; no Web IDL special behavior involved
487-
[9007199254740994, 9007199254740994],
488-
[4611686018427388000, 4611686018427388000],
489-
[9223372036854776000, 9223372036854776000],
490-
[-4294967295, 18446744069414584321],
491-
[-9007199254740991, 18437736874454810625]
502+
[4294967296, 4294967296], // 2**32
503+
[9007199254740991, 9007199254740991], // 2**53 - 1 = Number.MAX_SAFE_INTEGER
504+
[9007199254740992, 9007199254740992], // 2**53
505+
[9007199254740993, 9007199254740992], // This is just JavaScript, as 9007199254740993 === 9007199254740992
506+
[9007199254740994, 9007199254740994], // 2**53 + 2
507+
[4611686018427387904, 4611686018427387904], // 2**62
508+
[9223372036854775808, 9223372036854775808], // 2**63
509+
[9223372036854777856, 9223372036854777856], // 2**63 + 2**11
510+
[18446744073709551616, 0], // 2**64
511+
[18446744073709555712, 4096], // 2**64 + 2**12
512+
[-4294967296, 18446744069414584320], // -2**32
513+
[-9007199254740991, 18437736874454810624], // -(2**53 - 1) = Number.MIN_SAFE_INTEGER
514+
[-9007199254740992, 18437736874454810624], // -(2**53)
515+
[-18446744073709551616, 0], // -(2**64)
516+
[-18446744073709555712, 18446744073709547520] // -(2**64 + 2**12)
492517
]);
493518

494-
generateTests(sut, [
495-
[4294967296, 4294967296],
496-
[9007199254740991, 9007199254740991],
497-
[9007199254740992, 9007199254740991],
498-
[9007199254740993, 9007199254740991],
499-
[9007199254740994, 9007199254740991],
500-
[4611686018427388000, 9007199254740991],
501-
[9223372036854776000, 9007199254740991],
502-
[18446744073709552000, 9007199254740991],
503-
[-4294967295, 0],
504-
[-9007199254740991, 0],
505-
[-9007199254740992, 0],
506-
[-18446744073709552000, 0]
507-
], { clamp: true }, "with [Clamp]");
508-
509-
generateTests(sut, [
510-
[4294967296, 4294967296],
511-
[9007199254740991, 9007199254740991],
512-
[9007199254740992, TypeError],
513-
[9007199254740993, TypeError],
514-
[9007199254740994, TypeError],
515-
[4611686018427388000, TypeError],
516-
[9223372036854776000, TypeError],
517-
[18446744073709552000, TypeError],
518-
[-4294967295, TypeError],
519-
[-9007199254740991, TypeError],
520-
[-9007199254740992, TypeError],
521-
[-18446744073709552000, TypeError]
522-
], { enforceRange: true }, "with [EnforceRange]");
523-
});
524-
525-
describe.skip("WebIDL unsigned long long test cases hard to pass within JavaScript", () => {
526-
// See the README for more details.
527-
const sut = conversions["unsigned long long"];
519+
describe("[Clamp]", () => {
520+
generateTests(sut, [
521+
[4294967296, 4294967296], // 2**32
522+
[9007199254740991, Number.MAX_SAFE_INTEGER], // 2**53 - 1 = Number.MAX_SAFE_INTEGER
523+
[9007199254740992, Number.MAX_SAFE_INTEGER], // 2**53
524+
[9007199254740993, Number.MAX_SAFE_INTEGER], // 2**53 + 1
525+
[9007199254740994, Number.MAX_SAFE_INTEGER], // 2**53 + 2
526+
[4611686018427387904, Number.MAX_SAFE_INTEGER], // 2**62
527+
[9223372036854775808, Number.MAX_SAFE_INTEGER], // 2**63
528+
[9223372036854777856, Number.MAX_SAFE_INTEGER], // 2**63 + 2**11
529+
[18446744073709551616, Number.MAX_SAFE_INTEGER], // 2**64
530+
[18446744073709555712, Number.MAX_SAFE_INTEGER], // 2**64 + 2**12
531+
[-4294967296, 0], // -2**32
532+
[-9007199254740991, 0], // -(2**53 - 1) = Number.MIN_SAFE_INTEGER
533+
[-9007199254740992, 0], // -(2**53)
534+
[-18446744073709551616, 0], // -(2**64)
535+
[-18446744073709555712, 0] // -(2**64 + 2**12)
536+
], { clamp: true }, "with [Clamp]");
537+
});
528538

529-
generateTests(sut, [
530-
[18446744073709552000, -18446744073709552000]
531-
]);
539+
describe("[EnforceRange]", () => {
540+
generateTests(sut, [
541+
[4294967296, 4294967296], // 2**32
542+
[9007199254740991, Number.MAX_SAFE_INTEGER], // 2**53 - 1 = Number.MAX_SAFE_INTEGER
543+
[9007199254740992, TypeError], // 2**53
544+
[9007199254740993, TypeError], // 2**53 + 1
545+
[9007199254740994, TypeError], // 2**53 + 2
546+
[4611686018427387904, TypeError], // 2**62
547+
[9223372036854775808, TypeError], // 2**63
548+
[9223372036854777856, TypeError], // 2**63 + 2**11
549+
[18446744073709551616, TypeError], // 2**64
550+
[18446744073709555712, TypeError], // 2**64 + 2**12
551+
[-4294967296, TypeError], // -2**32
552+
[-9007199254740991, TypeError], // -(2**53 - 1) = Number.MIN_SAFE_INTEGER
553+
[-9007199254740992, TypeError], // -(2**53)
554+
[-18446744073709551616, TypeError], // -(2**64)
555+
[-18446744073709555712, TypeError] // -(2**64 + 2**12)
556+
], { enforceRange: true }, "with [EnforceRange]");
557+
});
532558
});

0 commit comments

Comments
 (0)