Skip to content

[Draft] Experimental fork: replace native usage of BigInt with JSBI. #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions babel.config.js

This file was deleted.

15 changes: 15 additions & 0 deletions babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"comments": false,
"presets": [
"@babel/preset-env"
],
"targets": {
"node": 18,
"browsers": [
"> 2%",
"not ie 11",
"not op_mini all"
]
},
"plugins": ["transform-jsbi-to-bigint"]
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "js-xdr",
"version": "3.0.0",
"description": "Read/write XDR encoded data structures (RFC 4506)",
"main": "lib/xdr.js",
"browser": "dist/xdr.js",
"main": "lib/xdr.min.js",
"browser": "dist/xdr.min.js",
"module": "src/index.js",
"scripts": {
"build": "yarn run build:browser && yarn run build:node",
Expand Down Expand Up @@ -53,6 +53,7 @@
"@babel/preset-env": "^7.21.4",
"@babel/register": "^7.14.5",
"babel-loader": "^9.1.2",
"babel-plugin-transform-jsbi-to-bigint": "^1.4.0",
"buffer": "^6.0.3",
"chai": "^4.3.7",
"eslint": "^8.43.0",
Expand All @@ -77,5 +78,8 @@
"terser-webpack-plugin": "^5.3.7",
"webpack": "^5.88.0",
"webpack-cli": "^5.0.2"
},
"dependencies": {
"jsbi": "^4.3.0"
}
}
61 changes: 40 additions & 21 deletions src/bigint-encoder.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import JSBI from 'jsbi';

/**
* Encode a native `bigint` value from a list of arbitrary integer-like values.
*
* @param {Array<number|bigint|string>} parts - Slices to encode in big-endian
* format (i.e. earlier elements are higher bits)
* @param {64|128|256} size - Number of bits in the target integer type
* @param {boolean} unsigned - Whether it's an unsigned integer
* @param {Array<number|bigint|string|JSBI.BigInt>} parts slices to encode in
* big-endian format (i.e. earlier elements are higher bits)
* @param {64|128|256} size number of bits in the target integer type
* @param {boolean} unsigned whether it's an unsigned integer
*
* @returns {bigint}
* @returns {JSBI.BigInt}
*/
export function encodeBigIntFromBits(parts, size, unsigned) {
/** @type {Array<JSBI.BigInt>} */
if (!(parts instanceof Array)) {
// allow a single parameter instead of an array
parts = [parts];
Expand All @@ -28,15 +31,15 @@ export function encodeBigIntFromBits(parts, size, unsigned) {

default:
throw new RangeError(
`expected slices to fit in 32/64/128/256 bits, got ${parts}`
`expected slices to fit in 32/64/128/256 bits, got ${sliceSize}: ${parts}`
);
}

// normalize all inputs to bigint
try {
for (let i = 0; i < parts.length; i++) {
if (typeof parts[i] !== 'bigint') {
parts[i] = BigInt(parts[i].valueOf());
parts[i] = JSBI.BigInt(parts[i].valueOf());
}
}
} catch (e) {
Expand All @@ -46,19 +49,26 @@ export function encodeBigIntFromBits(parts, size, unsigned) {
// check for sign mismatches for single inputs (this is a special case to
// handle one parameter passed to e.g. UnsignedHyper et al.)
// see https://github.com/stellar/js-xdr/pull/100#discussion_r1228770845
if (unsigned && parts.length === 1 && parts[0] < 0n) {
if (unsigned && parts.length === 1 && JSBI.LT(parts[0], JSBI.BigInt(0))) {
throw new RangeError(`expected a positive value, got: ${parts}`);
}

// encode in big-endian fashion, shifting each slice by the slice size
let result = BigInt.asUintN(sliceSize, parts[0]); // safe: len >= 1
let result = JSBI.asUintN(sliceSize, parts[0]); // safe: len >= 1
for (let i = 1; i < parts.length; i++) {
result |= BigInt.asUintN(sliceSize, parts[i]) << BigInt(i * sliceSize);
// result |= parts[i] << (i * sliceSize)
result = JSBI.bitwiseOr(
result,
JSBI.leftShift(
JSBI.asUintN(sliceSize, parts[i]),
JSBI.BigInt(i * sliceSize)
)
);
}

// interpret value as signed if necessary and clamp it
if (!unsigned) {
result = BigInt.asIntN(size, result);
result = JSBI.asIntN(size, result);
}

// check boundaries
Expand Down Expand Up @@ -86,7 +96,7 @@ export function encodeBigIntFromBits(parts, size, unsigned) {
* @return {bigint[]}
*/
export function sliceBigInt(value, iSize, sliceSize) {
if (typeof value !== 'bigint') {
if (!(value instanceof JSBI)) {
throw new TypeError(`Expected bigint 'value', got ${typeof value}`);
}

Expand All @@ -105,17 +115,17 @@ export function sliceBigInt(value, iSize, sliceSize) {
);
}

const shift = BigInt(sliceSize);
const shift = JSBI.BigInt(sliceSize);

// iterate shift and mask application
const result = new Array(total);
for (let i = 0; i < total; i++) {
// we force a signed interpretation to preserve sign in each slice value,
// but downstream can convert to unsigned if it's appropriate
result[i] = BigInt.asIntN(sliceSize, value); // clamps to size
result[i] = JSBI.asIntN(sliceSize, value); // clamps to size

// move on to the next chunk
value >>= shift;
value = JSBI.signedRightShift(value, shift);
}

return result;
Expand All @@ -127,15 +137,24 @@ export function formatIntName(precision, unsigned) {

/**
* Get min|max boundaries for an integer with a specified bits size
* @param {64|128|256} size - Number of bits in the source integer type
* @param {Boolean} unsigned - Whether it's an unsigned integer
* @return {BigInt[]}
* @param {64|128|256} size number of bits in the source integer type
* @param {Boolean} unsigned whether it's an unsigned integer
* @return {JSBI.BigInt[]}
*/
export function calculateBigIntBoundaries(size, unsigned) {
if (unsigned) {
return [0n, (1n << BigInt(size)) - 1n];
return [
JSBI.BigInt(0),
JSBI.subtract(
JSBI.leftShift(JSBI.BigInt(1), JSBI.BigInt(size)),
JSBI.BigInt(1)
)
];
}

const boundary = 1n << BigInt(size - 1);
return [0n - boundary, boundary - 1n];
const boundary = JSBI.leftShift(JSBI.BigInt(1), JSBI.BigInt(size - 1));
return [
JSBI.subtract(JSBI.BigInt(0), boundary),
JSBI.subtract(boundary, JSBI.BigInt(1))
];
}
12 changes: 3 additions & 9 deletions src/int.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ const MAX_VALUE = 2147483647;
const MIN_VALUE = -2147483648;

export class Int extends XdrPrimitiveType {
/**
* @inheritDoc
*/
/** @inheritDoc */
static read(reader) {
return reader.readInt32BE();
}

/**
* @inheritDoc
*/
/** @inheritDoc */
static write(value, writer) {
if (typeof value !== 'number')
throw new XdrWriterError('not a number');
Expand All @@ -25,9 +21,7 @@ export class Int extends XdrPrimitiveType {
writer.writeInt32BE(value);
}

/**
* @inheritDoc
*/
/** @inheritDoc */
static isValid(value) {
if (typeof value !== 'number' || (value | 0) !== value) {
return false;
Expand Down
62 changes: 40 additions & 22 deletions src/large-int.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import JSBI from 'jsbi';

import { XdrPrimitiveType } from './xdr-type';
import { calculateBigIntBoundaries, encodeBigIntFromBits, sliceBigInt } from './bigint-encoder';
import {
calculateBigIntBoundaries,
encodeBigIntFromBits,
sliceBigInt
} from './bigint-encoder';
import { XdrNotImplementedDefinitionError, XdrWriterError } from './errors';

export class LargeInt extends XdrPrimitiveType {
/** @type {JSBI.BigInt} */
_value;

/**
* @param {Array<Number|BigInt|String>} parts - Slices to encode
* @param {Array<Number|BigInt|String|JSBI.BigInt>} parts slices to encode
*/
constructor(args) {
super();
Expand Down Expand Up @@ -43,33 +52,37 @@ export class LargeInt extends XdrPrimitiveType {
}

toJSON() {
return {_value: this._value.toString()}
return { _value: this._value.toString() };
}

toBigInt() {
return BigInt(this._value);
return JSBI.BigInt(this._value);
}

/**
* @inheritDoc
*/
/** @inheritDoc */
static read(reader) {
const {size} = this.prototype;
if (size === 64)
return new this(reader.readBigUInt64BE());
return new this(...Array.from({length: size / 64}, () => reader.readBigUInt64BE()).reverse());
const { size } = this.prototype;
if (size === 64) return new this(reader.readBigUInt64BE());
return new this(
...Array.from({ length: size / 64 }, () =>
reader.readBigUInt64BE()
).reverse()
);
}

/**
* @inheritDoc
*/
/** @inheritDoc */
static write(value, writer) {
if (value instanceof this) {
value = value._value;
} else if (typeof value !== 'bigint' || value > this.MAX_VALUE || value < this.MIN_VALUE)
} else if (
!(value instanceof JSBI) ||
value > this.MAX_VALUE ||
value < this.MIN_VALUE
) {
throw new XdrWriterError(`${value} is not a ${this.name}`);
}

const {unsigned, size} = this.prototype;
const { unsigned, size } = this.prototype;
if (size === 64) {
if (unsigned) {
writer.writeBigUInt64BE(value);
Expand All @@ -91,7 +104,10 @@ export class LargeInt extends XdrPrimitiveType {
* @inheritDoc
*/
static isValid(value) {
return typeof value === 'bigint' || (value instanceof this);
return (
value instanceof JSBI ||
value instanceof this
);
}

/**
Expand All @@ -103,17 +119,19 @@ export class LargeInt extends XdrPrimitiveType {
return new this(string);
}

static MAX_VALUE = 0n;

static MIN_VALUE = 0n;
static MAX_VALUE = JSBI.BigInt(0);
static MIN_VALUE = JSBI.BigInt(0);

/**
* @internal
* @return {void}
*/
static defineIntBoundaries() {
const [min, max] = calculateBigIntBoundaries(this.prototype.size, this.prototype.unsigned);
const [min, max] = calculateBigIntBoundaries(
this.prototype.size,
this.prototype.unsigned
);
this.MIN_VALUE = min;
this.MAX_VALUE = max;
}
}
}
1 change: 1 addition & 0 deletions test/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ global.sinon = require('sinon');
global.chai.use(require('sinon-chai'));

global.expect = global.chai.expect;
global.JSBI = require('jsbi');

exports.mochaHooks = {
beforeEach: function () {
Expand Down
Loading