Skip to content

Commit 7e599c5

Browse files
committed
Unaligned bit arrays on the JavaScript target
1 parent f15d41b commit 7e599c5

File tree

5 files changed

+149
-80
lines changed

5 files changed

+149
-80
lines changed

CHANGELOG.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Unaligned bit arrays on the JavaScript target are now supported by the
6+
following functions in the `bit_array` module: `append`, `bit_size`,
7+
`compare`, `concat`, `inspect`, `starts_with`. Note: unaligned bit arrays on
8+
JavaScript are supported starting with Gleam v1.7.
9+
310
## v0.47.0 - 2024-12-10
411

5-
- The `compare` and `to_string` functions from the `gleam/bool` module have been
12+
- The `compare` and `to_int` functions from the `gleam/bool` module have been
613
deprecated.
714

815
## v0.46.0 - 2024-12-08

src/gleam/bit_array.gleam

+4-6
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ pub fn from_string(x: String) -> BitArray
1313
/// Returns an integer which is the number of bits in the bit array.
1414
///
1515
@external(erlang, "erlang", "bit_size")
16-
pub fn bit_size(x: BitArray) -> Int {
17-
byte_size(x) * 8
18-
}
16+
@external(javascript, "../gleam_stdlib.mjs", "bit_array_bit_size")
17+
pub fn bit_size(x: BitArray) -> Int
1918

2019
/// Returns an integer which is the number of bytes in the bit array.
2120
///
@@ -26,9 +25,8 @@ pub fn byte_size(x: BitArray) -> Int
2625
/// Pads a bit array with zeros so that it is a whole number of bytes.
2726
///
2827
@external(erlang, "gleam_stdlib", "bit_array_pad_to_bytes")
29-
pub fn pad_to_bytes(x: BitArray) -> BitArray {
30-
x
31-
}
28+
@external(javascript, "../gleam_stdlib.mjs", "bit_array_pad_to_bytes")
29+
pub fn pad_to_bytes(x: BitArray) -> BitArray
3230

3331
/// Creates a new bit array by joining two bit arrays.
3432
///

src/gleam_stdlib.mjs

+132-15
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,49 @@ export function bit_array_from_string(string) {
320320
return toBitArray([stringBits(string)]);
321321
}
322322

323+
export function bit_array_bit_size(bit_array) {
324+
if (bit_array.bitSize === undefined) {
325+
return bit_array.length * 8;
326+
}
327+
328+
return bit_array.bitSize;
329+
}
330+
331+
export function bit_array_pad_to_bytes(bit_array) {
332+
// If the bit array is byte aligned it can be returned unchanged
333+
const trailingBitsCount = bit_array_bit_size(bit_array) % 8;
334+
if (trailingBitsCount === 0) {
335+
return bit_array;
336+
}
337+
338+
const finalByte = bit_array.buffer[bit_array.length - 1];
339+
340+
let correctFinalByte = finalByte;
341+
correctFinalByte >>= 8 - trailingBitsCount;
342+
correctFinalByte <<= 8 - trailingBitsCount;
343+
344+
// If the unused bits in the final byte are already set to zero then the
345+
// existing buffer can be re-used, avoiding a copy
346+
if (finalByte === correctFinalByte) {
347+
return new BitArray(bit_array.buffer);
348+
}
349+
350+
// Copy the bit array into a new buffer and set the correct final byte
351+
const newBuffer = bit_array.buffer.slice();
352+
newBuffer[newBuffer.length - 1] = correctFinalByte;
353+
354+
return new BitArray(newBuffer);
355+
}
356+
357+
const BIT_ARRAY_UNALIGNED_SUPPORTED =
358+
new BitArray(new Uint8Array()).bitSize !== undefined;
359+
323360
export function bit_array_concat(bit_arrays) {
324-
return toBitArray(bit_arrays.toArray().map((b) => b.buffer));
361+
if (BIT_ARRAY_UNALIGNED_SUPPORTED) {
362+
return toBitArray(bit_arrays.toArray());
363+
} else {
364+
return toBitArray(bit_arrays.toArray().map((b) => b.buffer));
365+
}
325366
}
326367

327368
export function console_log(term) {
@@ -337,6 +378,10 @@ export function crash(message) {
337378
}
338379

339380
export function bit_array_to_string(bit_array) {
381+
if (bit_array_bit_size(bit_array) % 8 !== 0) {
382+
return new Error(Nil);
383+
}
384+
340385
try {
341386
const decoder = new TextDecoder("utf-8", { fatal: true });
342387
return new Ok(decoder.decode(bit_array.buffer));
@@ -419,13 +464,19 @@ export function random_uniform() {
419464
export function bit_array_slice(bits, position, length) {
420465
const start = Math.min(position, position + length);
421466
const end = Math.max(position, position + length);
422-
if (start < 0 || end > bits.length) return new Error(Nil);
467+
468+
if (start < 0 || end * 8 > bit_array_bit_size(bits)) {
469+
return new Error(Nil);
470+
}
471+
423472
const byteOffset = bits.buffer.byteOffset + start;
473+
424474
const buffer = new Uint8Array(
425475
bits.buffer.buffer,
426476
byteOffset,
427477
Math.abs(length),
428478
);
479+
429480
return new Ok(new BitArray(buffer));
430481
}
431482

@@ -972,41 +1023,107 @@ export function base16_decode(string) {
9721023
}
9731024

9741025
export function bit_array_inspect(bits, acc) {
975-
return `${acc}${[...bits.buffer].join(", ")}`;
1026+
const bitSize = bit_array_bit_size(bits);
1027+
1028+
if (bitSize % 8 === 0) {
1029+
return `${acc}${[...bits.buffer].join(", ")}`;
1030+
}
1031+
1032+
for (let i = 0; i < bits.length - 1; i++) {
1033+
acc += bits.buffer[i].toString();
1034+
acc += ", ";
1035+
}
1036+
1037+
const trailingBitsCount = bitSize % 8;
1038+
acc += bits.buffer[bits.length - 1] >> (8 - trailingBitsCount);
1039+
acc += `:size(${trailingBitsCount})`;
1040+
1041+
return acc;
9761042
}
9771043

9781044
export function bit_array_compare(first, second) {
979-
for (let i = 0; i < first.length; i++) {
980-
if (i >= second.length) {
981-
return new Gt(); // first has more items
982-
}
1045+
let i = 0;
1046+
1047+
let firstSize = bit_array_bit_size(first);
1048+
let secondSize = bit_array_bit_size(second);
1049+
1050+
while (firstSize >= 8 && secondSize >= 8) {
9831051
const f = first.buffer[i];
9841052
const s = second.buffer[i];
1053+
9851054
if (f > s) {
9861055
return new Gt();
987-
}
988-
if (f < s) {
1056+
} else if (f < s) {
9891057
return new Lt();
9901058
}
1059+
1060+
i++;
1061+
firstSize -= 8;
1062+
secondSize -= 8;
9911063
}
992-
// This means that either first did not have any items
993-
// or all items in first were equal to second.
994-
if (first.length === second.length) {
1064+
1065+
if (firstSize === 0 && secondSize === 0) {
9951066
return new Eq();
9961067
}
997-
return new Lt(); // second has more items
1068+
1069+
// First has more items, example: "AB" > "A":
1070+
if (secondSize === 0) {
1071+
return new Gt();
1072+
}
1073+
1074+
// Second has more items, example: "A" < "AB":
1075+
if (firstSize === 0) {
1076+
return new Lt();
1077+
}
1078+
1079+
// This happens when there are unaligned bit arrays
1080+
1081+
const f = first.buffer[i] >> (8 - firstSize);
1082+
const s = second.buffer[i] >> (8 - secondSize);
1083+
1084+
if (f > s) {
1085+
return new Gt();
1086+
}
1087+
if (f < s) {
1088+
return new Lt();
1089+
}
1090+
if (firstSize > secondSize) {
1091+
return new Gt();
1092+
}
1093+
if (firstSize < secondSize) {
1094+
return new Lt();
1095+
}
1096+
1097+
return new Eq();
9981098
}
9991099

10001100
export function bit_array_starts_with(bits, prefix) {
1001-
if (prefix.length > bits.length) {
1101+
const prefixSize = bit_array_bit_size(prefix);
1102+
1103+
if (prefixSize > bit_array_bit_size(bits)) {
10021104
return false;
10031105
}
10041106

1005-
for (let i = 0; i < prefix.length; i++) {
1107+
const isPrefixAligned = prefixSize % 8 === 0;
1108+
1109+
// Check any whole bytes
1110+
const byteCount = isPrefixAligned ? prefix.length : prefix.length - 1;
1111+
for (let i = 0; i < byteCount; i++) {
10061112
if (bits.buffer[i] !== prefix.buffer[i]) {
10071113
return false;
10081114
}
10091115
}
10101116

1117+
// Check any trailing bits at the end of the prefix
1118+
if (!isPrefixAligned) {
1119+
const unusedBitsCount = 8 - (prefixSize % 8);
1120+
if (
1121+
bits.buffer[prefix.length - 1] >> unusedBitsCount !==
1122+
prefix.buffer[prefix.length - 1] >> unusedBitsCount
1123+
) {
1124+
return false;
1125+
}
1126+
}
1127+
10111128
return true;
10121129
}

0 commit comments

Comments
 (0)