Skip to content

Commit

Permalink
fix: BigInt exponentiation transpiler error (@dfinity/candid) (#599)
Browse files Browse the repository at this point in the history
* Fix edge case of log2 function on BigInt bit count

* Increase ilog2 input value strictness

* Convert (BigInt(2) ** BigInt(n)) to iexp2(n)
  • Loading branch information
rvanasa committed Jul 28, 2022
1 parent 8fc0b17 commit 4c676af
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 3 deletions.
7 changes: 4 additions & 3 deletions packages/candid/src/idl.ts
Expand Up @@ -15,6 +15,7 @@ import {
writeIntLE,
writeUIntLE,
} from './utils/leb128';
import { iexp2 } from './utils/bigint-math';

// tslint:disable:max-line-length
/**
Expand Down Expand Up @@ -630,8 +631,8 @@ export class FixedIntClass extends PrimitiveType<bigint | number> {
}

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)) {
Expand Down Expand Up @@ -683,7 +684,7 @@ export class FixedNatClass extends PrimitiveType<bigint | number> {
}

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) {
Expand Down
30 changes: 30 additions & 0 deletions 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);
}
});
28 changes: 28 additions & 0 deletions 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;
}

0 comments on commit 4c676af

Please sign in to comment.