diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d73152721a..a18ab4ca9b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -386,6 +386,12 @@ annotation of a parameter of an anonymous function would do nothing. ([Surya Rose](https://github.com/GearsDatapacks)) +- Fixed a bug where an incorrect bit array would be generated on JavaScript for + negative `Int` values when the segment's `size` was wider than 48 bits or when + the `Int` value was less than the minimum representable value for the segment + size. + ([Richard Viney](https://github.com/richard-viney)) + ## v1.5.1 - 2024-09-26 ### Bug Fixes diff --git a/compiler-core/templates/prelude.mjs b/compiler-core/templates/prelude.mjs index d2adeef273f..adc06636fc4 100644 --- a/compiler-core/templates/prelude.mjs +++ b/compiler-core/templates/prelude.mjs @@ -190,7 +190,7 @@ export function toBitArray(segments) { // @internal // Derived from this answer https://stackoverflow.com/questions/8482309/converting-javascript-integer-to-byte-array-and-back export function sizedInt(value, size, isBigEndian) { - if (size < 0) { + if (size <= 0) { return new Uint8Array(); } if (size % 8 != 0) { @@ -200,22 +200,38 @@ export function sizedInt(value, size, isBigEndian) { const byteArray = new Uint8Array(size / 8); - // Convert negative number to two's complement representation + let byteModulus = 256; + + // Convert negative numbers to two's complement representation. if (value < 0) { - value = 2 ** size + value; + let valueModulus; + + // For output sizes larger than 48 bits BigInt is used in order to + // maintain accuracy + if (size <= 48) { + valueModulus = 2 ** size; + } else { + valueModulus = 1n << BigInt(size); + + value = BigInt(value); + byteModulus = BigInt(byteModulus); + } + + value %= valueModulus; + value = valueModulus + value; } if (isBigEndian) { for (let i = byteArray.length - 1; i >= 0; i--) { - const byte = value % 256; - byteArray[i] = byte; - value = (value - byte) / 256; + const byte = value % byteModulus; + byteArray[i] = Number(byte); + value = (value - byte) / byteModulus; } } else { for (let i = 0; i < byteArray.length; i++) { - const byte = value % 256; - byteArray[i] = byte; - value = (value - byte) / 256; + const byte = value % byteModulus; + byteArray[i] = Number(byte); + value = (value - byte) / byteModulus; } } diff --git a/test/javascript_prelude/main.mjs b/test/javascript_prelude/main.mjs index 0f9e8289d59..05f5a07d277 100755 --- a/test/javascript_prelude/main.mjs +++ b/test/javascript_prelude/main.mjs @@ -12,6 +12,7 @@ import { stringBits, toBitArray, toList, + sizedInt, } from "./prelude.mjs"; let failures = 0; @@ -394,6 +395,8 @@ assertNotEqual(new HasCustomEquals(1, 1), new HasCustomEquals(2, 1)); assertEqual(hasEqualsField, { ...hasEqualsField }); assertNotEqual(hasEqualsField, hasEqualsField2); +// BitArray + assertEqual(new BitArray(new Uint8Array([1, 2, 3])).byteAt(0), 1); assertEqual(new BitArray(new Uint8Array([1, 2, 3])).byteAt(2), 3); assertEqual(new BitArray(new Uint8Array([1, 2, 3])).intFromSlice(0, 1, true, false), 1); @@ -424,6 +427,49 @@ assertEqual( new BitArray(new Uint8Array([2, 3])), ); +// sizedInt() + +assertEqual( + sizedInt(100, 0, true), + new Uint8Array([]), +); +assertEqual( + sizedInt(0, 32, true), + new Uint8Array([0, 0, 0, 0]), +); +assertEqual( + sizedInt(1, 24, true), + new Uint8Array([0, 0, 1]), +); +assertEqual( + sizedInt(-1, 32, true), + new Uint8Array([255, 255, 255, 255]), +); +assertEqual( + sizedInt(80000, 16, true), + new Uint8Array([56, 128]), +); +assertEqual( + sizedInt(-80000, 16, true), + new Uint8Array([199, 128]), +); +assertEqual( + sizedInt(-489_391_639_457_909_760, 56, true), + new Uint8Array([53, 84, 229, 150, 16, 180, 0]), +); +assertEqual( + sizedInt(-1, 64, true), + new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]), +); +assertEqual( + sizedInt(Number.MAX_SAFE_INTEGER, 64, true), + new Uint8Array([0, 31, 255, 255, 255, 255, 255, 255]), +); +assertEqual( + sizedInt(Number.MIN_SAFE_INTEGER, 64, true), + new Uint8Array([255, 224, 0, 0, 0, 0, 0, 1]), +); + // Result.isOk assertEqual(new Ok(1).isOk(), true);