From f8df1f2ab823184ee4e1776b1316d854b7fb6765 Mon Sep 17 00:00:00 2001 From: Ryan Vandersmith Date: Sun, 24 Jul 2022 12:38:01 -0600 Subject: [PATCH 1/3] Fix edge case of log2 function on BigInt bit count --- packages/candid/src/idl.ts | 5 +++-- packages/candid/src/utils/ilog2.test.ts | 10 ++++++++++ packages/candid/src/utils/ilog2.ts | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 packages/candid/src/utils/ilog2.test.ts create mode 100644 packages/candid/src/utils/ilog2.ts diff --git a/packages/candid/src/idl.ts b/packages/candid/src/idl.ts index c8953c434..4095be5f3 100644 --- a/packages/candid/src/idl.ts +++ b/packages/candid/src/idl.ts @@ -15,6 +15,7 @@ import { writeIntLE, writeUIntLE, } from './utils/leb128'; +import { ilog2 } from './utils/ilog2'; // tslint:disable:max-line-length /** @@ -647,7 +648,7 @@ export class FixedIntClass extends PrimitiveType { } public encodeType() { - const offset = Math.log2(this._bits) - 3; + const offset = ilog2(this._bits) - 3; return slebEncode(-9 - offset); } @@ -699,7 +700,7 @@ export class FixedNatClass extends PrimitiveType { } public encodeType() { - const offset = Math.log2(this._bits) - 3; + const offset = ilog2(this._bits) - 3; return slebEncode(-5 - offset); } diff --git a/packages/candid/src/utils/ilog2.test.ts b/packages/candid/src/utils/ilog2.test.ts new file mode 100644 index 000000000..7f8a29ef7 --- /dev/null +++ b/packages/candid/src/utils/ilog2.test.ts @@ -0,0 +1,10 @@ +import { ilog2 } from './ilog2'; + +test('log2', () => { + for (let n = -10; n < 100; n++) { + expect(ilog2(n)).toBe(n > 0 ? Math.floor(Math.log2(n)) : NaN); + } + for (const p of [0, 1, 3, 55, 10000]) { + expect(ilog2(BigInt(2) ** BigInt(p))).toBe(p); + } +}); diff --git a/packages/candid/src/utils/ilog2.ts b/packages/candid/src/utils/ilog2.ts new file mode 100644 index 000000000..ba6df597d --- /dev/null +++ b/packages/candid/src/utils/ilog2.ts @@ -0,0 +1,8 @@ +/** + * Equivalent to `Math.log2` with support for `BigInt` values + * + * @param n number or bigint + */ +export function ilog2(n: bigint | number) { + return n > 0 ? BigInt(n).toString(2).length - 1 : NaN; +} From 27b81f3aa49b80c08e0f87db51d86f7ba293d0ba Mon Sep 17 00:00:00 2001 From: Ryan Vandersmith Date: Mon, 25 Jul 2022 10:05:53 -0600 Subject: [PATCH 2/3] Increase ilog2 input value strictness --- packages/candid/src/utils/ilog2.test.ts | 7 ++++++- packages/candid/src/utils/ilog2.ts | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/candid/src/utils/ilog2.test.ts b/packages/candid/src/utils/ilog2.test.ts index 7f8a29ef7..f83c3b388 100644 --- a/packages/candid/src/utils/ilog2.test.ts +++ b/packages/candid/src/utils/ilog2.test.ts @@ -1,10 +1,15 @@ import { ilog2 } from './ilog2'; -test('log2', () => { +test('ilog2', () => { for (let n = -10; n < 100; n++) { expect(ilog2(n)).toBe(n > 0 ? Math.floor(Math.log2(n)) : NaN); } for (const p of [0, 1, 3, 55, 10000]) { expect(ilog2(BigInt(2) ** BigInt(p))).toBe(p); } + + expect(() => ilog2(1.5)).toThrow( + 'The number 1.5 cannot be converted to a BigInt because it is not an integer', + ); + expect(() => (ilog2 as (string) => number)('abc')).toThrow('Cannot convert abc to a BigInt'); }); diff --git a/packages/candid/src/utils/ilog2.ts b/packages/candid/src/utils/ilog2.ts index ba6df597d..301bc20e7 100644 --- a/packages/candid/src/utils/ilog2.ts +++ b/packages/candid/src/utils/ilog2.ts @@ -4,5 +4,6 @@ * @param n number or bigint */ export function ilog2(n: bigint | number) { - return n > 0 ? BigInt(n).toString(2).length - 1 : NaN; + const nBig = BigInt(n); + return nBig > 0 ? nBig.toString(2).length - 1 : NaN; } From 24acf8474dbb4ce94111de77315ceced736ad2d1 Mon Sep 17 00:00:00 2001 From: Ryan Vandersmith Date: Mon, 25 Jul 2022 13:17:53 -0600 Subject: [PATCH 3/3] Convert (BigInt(2) ** BigInt(n)) to iexp2(n) --- packages/candid/src/idl.ts | 12 ++++---- packages/candid/src/utils/bigint-math.test.ts | 30 +++++++++++++++++++ packages/candid/src/utils/bigint-math.ts | 28 +++++++++++++++++ packages/candid/src/utils/ilog2.test.ts | 15 ---------- packages/candid/src/utils/ilog2.ts | 9 ------ 5 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 packages/candid/src/utils/bigint-math.test.ts create mode 100644 packages/candid/src/utils/bigint-math.ts delete mode 100644 packages/candid/src/utils/ilog2.test.ts delete mode 100644 packages/candid/src/utils/ilog2.ts diff --git a/packages/candid/src/idl.ts b/packages/candid/src/idl.ts index 4095be5f3..08766f828 100644 --- a/packages/candid/src/idl.ts +++ b/packages/candid/src/idl.ts @@ -15,7 +15,7 @@ import { writeIntLE, writeUIntLE, } from './utils/leb128'; -import { ilog2 } from './utils/ilog2'; +import { iexp2 } from './utils/bigint-math'; // tslint:disable:max-line-length /** @@ -631,8 +631,8 @@ export class FixedIntClass extends PrimitiveType { } public covariant(x: any): x is bigint { - const min = BigInt(2) ** BigInt(this._bits - 1) * BigInt(-1); - const max = BigInt(2) ** BigInt(this._bits - 1) - BigInt(1); + const min = iexp2(this._bits - 1) * BigInt(-1); + const max = iexp2(this._bits - 1) - BigInt(1); if (typeof x === 'bigint') { return x >= min && x <= max; } else if (Number.isInteger(x)) { @@ -648,7 +648,7 @@ export class FixedIntClass extends PrimitiveType { } public encodeType() { - const offset = ilog2(this._bits) - 3; + const offset = Math.log2(this._bits) - 3; return slebEncode(-9 - offset); } @@ -684,7 +684,7 @@ export class FixedNatClass extends PrimitiveType { } public covariant(x: any): x is bigint { - const max = BigInt(2) ** BigInt(this._bits); + const max = iexp2(this._bits); if (typeof x === 'bigint' && x >= BigInt(0)) { return x < max; } else if (Number.isInteger(x) && x >= 0) { @@ -700,7 +700,7 @@ export class FixedNatClass extends PrimitiveType { } public encodeType() { - const offset = ilog2(this._bits) - 3; + const offset = Math.log2(this._bits) - 3; return slebEncode(-5 - offset); } diff --git a/packages/candid/src/utils/bigint-math.test.ts b/packages/candid/src/utils/bigint-math.test.ts new file mode 100644 index 000000000..b3194cd93 --- /dev/null +++ b/packages/candid/src/utils/bigint-math.test.ts @@ -0,0 +1,30 @@ +import { ilog2, iexp2 } from './bigint-math'; + +test('ilog2', () => { + for (let n = 1; n < 100; n++) { + expect(ilog2(n)).toBe(n > 0 ? Math.floor(Math.log2(n)) : NaN); + } + expect(() => ilog2(0)).toThrow('Input must be positive'); + expect(() => ilog2(-1)).toThrow('Input must be positive'); + expect(() => ilog2(1.5)).toThrow( + 'The number 1.5 cannot be converted to a BigInt because it is not an integer', + ); + expect(() => (ilog2 as (any) => number)('abc')).toThrow('Cannot convert abc to a BigInt'); +}); + +test('iexp2', () => { + for (let n = 0; n < 10; n++) { + expect(iexp2(n)).toBe(BigInt(2 ** n)); + } + expect(() => ilog2(-1)).toThrow('Input must be positive'); + expect(() => iexp2(1.5)).toThrow( + 'The number 1.5 cannot be converted to a BigInt because it is not an integer', + ); + expect(() => (ilog2 as (any) => number)('abc')).toThrow('Cannot convert abc to a BigInt'); +}); + +test('ilog2 and iexp2', () => { + for (const p of [0, 1, 3, 55, 10000]) { + expect(ilog2(iexp2(BigInt(p)))).toBe(p); + } +}); diff --git a/packages/candid/src/utils/bigint-math.ts b/packages/candid/src/utils/bigint-math.ts new file mode 100644 index 000000000..d91b798af --- /dev/null +++ b/packages/candid/src/utils/bigint-math.ts @@ -0,0 +1,28 @@ +/** + * Equivalent to `Math.log2(n)` with support for `BigInt` values + * + * @param n bigint or integer + * @returns integer + */ +export function ilog2(n: bigint | number): number { + const nBig = BigInt(n); + if (n <= 0) { + throw new RangeError('Input must be positive'); + } + return nBig.toString(2).length - 1; +} + +/** + * Equivalent to `2 ** n` with support for `BigInt` values + * (necessary for browser preprocessors which replace the `**` operator with `Math.pow`) + * + * @param n bigint or integer + * @returns bigint + */ +export function iexp2(n: bigint | number): bigint { + const nBig = BigInt(n); + if (n < 0) { + throw new RangeError('Input must be non-negative'); + } + return BigInt(1) << nBig; +} diff --git a/packages/candid/src/utils/ilog2.test.ts b/packages/candid/src/utils/ilog2.test.ts deleted file mode 100644 index f83c3b388..000000000 --- a/packages/candid/src/utils/ilog2.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ilog2 } from './ilog2'; - -test('ilog2', () => { - for (let n = -10; n < 100; n++) { - expect(ilog2(n)).toBe(n > 0 ? Math.floor(Math.log2(n)) : NaN); - } - for (const p of [0, 1, 3, 55, 10000]) { - expect(ilog2(BigInt(2) ** BigInt(p))).toBe(p); - } - - expect(() => ilog2(1.5)).toThrow( - 'The number 1.5 cannot be converted to a BigInt because it is not an integer', - ); - expect(() => (ilog2 as (string) => number)('abc')).toThrow('Cannot convert abc to a BigInt'); -}); diff --git a/packages/candid/src/utils/ilog2.ts b/packages/candid/src/utils/ilog2.ts deleted file mode 100644 index 301bc20e7..000000000 --- a/packages/candid/src/utils/ilog2.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Equivalent to `Math.log2` with support for `BigInt` values - * - * @param n number or bigint - */ -export function ilog2(n: bigint | number) { - const nBig = BigInt(n); - return nBig > 0 ? nBig.toString(2).length - 1 : NaN; -}