diff --git a/packages/candid/src/idl.ts b/packages/candid/src/idl.ts index c8953c434..08766f828 100644 --- a/packages/candid/src/idl.ts +++ b/packages/candid/src/idl.ts @@ -15,6 +15,7 @@ import { writeIntLE, writeUIntLE, } from './utils/leb128'; +import { iexp2 } from './utils/bigint-math'; // tslint:disable:max-line-length /** @@ -630,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)) { @@ -683,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) { 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; +}