Skip to content

Commit

Permalink
Fix bit array encoding of certain negative Int values on JavaScript
Browse files Browse the repository at this point in the history
  • Loading branch information
richard-viney committed Nov 4, 2024
1 parent 50b15f6 commit ecf550c
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 9 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 25 additions & 9 deletions compiler-core/templates/prelude.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}

Expand Down
46 changes: 46 additions & 0 deletions test/javascript_prelude/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
stringBits,
toBitArray,
toList,
sizedInt,
} from "./prelude.mjs";

let failures = 0;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions test/language/test/language_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,14 @@ fn bit_array_tests() -> List(Test) {
|> example(fn() { assert_equal(True, <<<<1>>:bits, 2>> == <<1, 2>>) }),
"<<1>> == <<1:int>>"
|> example(fn() { assert_equal(True, <<1>> == <<1:int>>) }),
"<<80_000:16>> == <<56, 128>>"
|> example(fn() { assert_equal(True, <<80_000:16>> == <<56, 128>>) }),
"<<-80_000:16>> == <<199, 128>>"
|> example(fn() { assert_equal(True, <<-80_000:16>> == <<199, 128>>) }),
"<<-1:64>> == <<255, 255, 255, 255, 255, 255, 255, 255>>"
|> example(fn() { assert_equal(True, <<-1:64>> == <<255, 255, 255, 255, 255, 255, 255, 255>>) }),
"<<-489_391_639_457_909_760:56>> == <<53, 84, 229, 150, 16, 180, 0>>"
|> example(fn() { assert_equal(True, <<-489_391_639_457_909_760:56>> == <<53, 84, 229, 150, 16, 180, 0>>) }),
"<<63, 240, 0, 0, 0, 0, 0, 0>> == <<1.0:float>>"
|> example(fn() {
assert_equal(True, <<63, 240, 0, 0, 0, 0, 0, 0>> == <<1.0:float>>)
Expand Down

0 comments on commit ecf550c

Please sign in to comment.