From 33fdd75f05a0aa256af86cbc8f3a0944071d42dc Mon Sep 17 00:00:00 2001 From: Richard Viney Date: Mon, 25 Nov 2024 17:05:31 +1300 Subject: [PATCH] Unaligned bit arrays on the JavaScript target --- CHANGELOG.md | 3 + compiler-core/src/javascript/expression.rs | 60 +- compiler-core/src/javascript/pattern.rs | 225 ++-- .../src/javascript/tests/bit_arrays.rs | 188 ++- .../src/javascript/tests/externals.rs | 16 +- ...t__tests__bit_arrays__as_module_const.snap | 4 +- ...__bit_arrays__bit_array_dynamic_slice.snap | 19 + ...ral_string_pattern_is_treated_as_utf8.snap | 4 +- ...__tests__bit_arrays__bit_array_sliced.snap | 17 + ...script__tests__bit_arrays__bit_string.snap | 2 +- ...__javascript__tests__bit_arrays__bits.snap | 2 +- ...arrays__bits_expression_requires_v1_7.snap | 25 + ...it_arrays__bits_pattern_requires_v1_7.snap | 25 + ...ipt__tests__bit_arrays__discard_sized.snap | 4 +- ...cript__tests__bit_arrays__empty_match.snap | 2 +- ..._tests__bit_arrays__match_binary_size.snap | 8 +- ...sts__bit_arrays__match_bits_with_size.snap | 29 + ...cript__tests__bit_arrays__match_bytes.snap | 2 +- ...ts__bit_arrays__match_bytes_with_size.snap | 29 + ...arrays__match_dynamic_bits_size_error.snap | 20 + ...cript__tests__bit_arrays__match_float.snap | 4 +- ...s__bit_arrays__match_float_big_endian.snap | 4 +- ...bit_arrays__match_float_little_endian.snap | 4 +- ..._tests__bit_arrays__match_float_sized.snap | 4 +- ..._arrays__match_float_sized_big_endian.snap | 4 +- ...rays__match_float_sized_little_endian.snap | 4 +- ...ys__match_non_byte_aligned_size_error.snap | 19 - ...__tests__bit_arrays__match_rest_bits.snap} | 8 +- ...bit_arrays__match_rest_bits_unaligned.snap | 29 + ..._tests__bit_arrays__match_rest_bytes.snap} | 4 +- ...ript__tests__bit_arrays__match_signed.snap | 4 +- ...arrays__match_signed_constant_pattern.snap | 2 +- ...cript__tests__bit_arrays__match_sized.snap | 4 +- ...s__bit_arrays__match_sized_big_endian.snap | 4 +- ...tch_sized_big_endian_constant_pattern.snap | 2 +- ...arrays__match_sized_big_endian_signed.snap | 4 +- ...ed_big_endian_signed_constant_pattern.snap | 2 +- ...rays__match_sized_big_endian_unsigned.snap | 4 +- ..._big_endian_unsigned_constant_pattern.snap | 2 +- ..._arrays__match_sized_constant_pattern.snap | 2 +- ...bit_arrays__match_sized_little_endian.snap | 4 +- ..._sized_little_endian_constant_pattern.snap | 2 +- ...ays__match_sized_little_endian_signed.snap | 4 +- ...little_endian_signed_constant_pattern.snap | 2 +- ...s__match_sized_little_endian_unsigned.snap | 4 +- ...ttle_endian_unsigned_constant_pattern.snap | 2 +- ...ts__bit_arrays__match_sized_unaligned.snap | 29 + ..._tests__bit_arrays__match_sized_value.snap | 4 +- ...s__match_sized_value_constant_pattern.snap | 2 +- ...pt__tests__bit_arrays__match_unsigned.snap | 2 +- ...rays__match_unsigned_constant_pattern.snap | 2 +- ...script__tests__bit_arrays__match_utf8.snap | 22 +- ...__tests__bit_arrays__not_byte_aligned.snap | 23 - ...rays__not_byte_aligned_explicit_sized.snap | 19 - ...bit_arrays__not_byte_aligned_variable.snap | 19 - ...__sized_bits_expression_requires_v1_7.snap | 25 + ...naligned_int_expression_requires_v1_7.snap | 25 + ...__unaligned_int_pattern_requires_v1_7.snap | 25 + ...tests__externals__erlang_bit_patterns.snap | 22 - compiler-core/src/type_/error.rs | 7 +- compiler-core/src/type_/expression.rs | 32 + compiler-core/src/type_/pattern.rs | 36 + compiler-core/src/type_/tests.rs | 30 +- ...external_module_with_at_requires_v1_2.snap | 2 +- compiler-core/src/warning.rs | 5 +- compiler-core/templates/prelude.d.mts | 36 +- compiler-core/templates/prelude.mjs | 1125 ++++++++++++++--- test/javascript_prelude/main.mjs | 432 ++++++- test/language/test/language_test.gleam | 388 ++++++ 69 files changed, 2558 insertions(+), 570 deletions(-) create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_dynamic_slice.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_sliced.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_expression_requires_v1_7.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_pattern_requires_v1_7.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bits_with_size.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes_with_size.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size_error.snap delete mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_non_byte_aligned_size_error.snap rename compiler-core/src/javascript/tests/snapshots/{gleam_core__javascript__tests__bit_arrays__match_rest_deprecated.snap => gleam_core__javascript__tests__bit_arrays__match_rest_bits.snap} (71%) create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits_unaligned.snap rename compiler-core/src/javascript/tests/snapshots/{gleam_core__javascript__tests__bit_arrays__match_rest.snap => gleam_core__javascript__tests__bit_arrays__match_rest_bytes.snap} (86%) create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_unaligned.snap delete mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned.snap delete mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_explicit_sized.snap delete mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_variable.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__sized_bits_expression_requires_v1_7.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_expression_requires_v1_7.snap create mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_pattern_requires_v1_7.snap delete mode 100644 compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__erlang_bit_patterns.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index e9677a04701..afbb53dcd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,9 @@ - On the JavaScript target, taking byte-aligned slices of bit arrays is now an O(1) operation instead of O(N), significantly improving performance. + +- On the JavaScript target, bit array expressions and patterns no longer need to + be byte aligned, and the `bits` segment type is now supported in patterns. ([Richard Viney](https://github.com/richard-viney)) ### Build tool diff --git a/compiler-core/src/javascript/expression.rs b/compiler-core/src/javascript/expression.rs index 20abb01ce49..e1d33c3e051 100644 --- a/compiler-core/src/javascript/expression.rs +++ b/compiler-core/src/javascript/expression.rs @@ -240,7 +240,8 @@ impl<'module> Generator<'module> { if segment.type_ == crate::type_::int() { match (details.size_value, segment.value.as_ref()) { (Some(size_value), TypedExpr::Int { int_value, .. }) - if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into() => + if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into() + && (&size_value % BigInt::from(8) == BigInt::ZERO) => { let bytes = bit_array_segment_int_value_to_bytes( int_value.clone(), @@ -295,7 +296,24 @@ impl<'module> Generator<'module> { } // Bit arrays - [Opt::Bytes { .. } | Opt::Bits { .. }] => Ok(docvec![value, ".buffer"]), + [Opt::Bits { .. }] => Ok(value), + + // Bit arrays with explicit size. The explicit size slices the bit array to the + // specified size. A runtime exception is thrown if the size exceeds the number + // of bits in the bit array. + [Opt::Bits { .. }, Opt::Size { value: size, .. }] + | [Opt::Size { value: size, .. }, Opt::Bits { .. }] => match &**size { + TypedExpr::Int { value: size, .. } => { + Ok(docvec![value, ".slice(0, ", size, ")"]) + } + + TypedExpr::Var { name, .. } => Ok(docvec![value, ".slice(0, ", name, ")"]), + + _ => Err(Error::Unsupported { + feature: "This bit array segment option UNREACHABLE".into(), + location: segment.location, + }), + }, // Anything else _ => Err(Error::Unsupported { @@ -348,15 +366,6 @@ impl<'module> Generator<'module> { _ => None, }; - if let Some(size_value) = size_value.as_ref() { - if *size_value > BigInt::ZERO && size_value % 8 != BigInt::ZERO { - return Err(Error::Unsupported { - feature: "Non byte aligned array".into(), - location: segment.location, - }); - } - } - ( size_value, self.not_in_tail_position(|gen| gen.wrap_expression(size))?, @@ -1460,7 +1469,8 @@ fn bit_array<'a>( if segment.type_ == crate::type_::int() { match (details.size_value, segment.value.as_ref()) { (Some(size_value), Constant::Int { int_value, .. }) - if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into() => + if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into() + && (&size_value % BigInt::from(8) == BigInt::ZERO) => { let bytes = bit_array_segment_int_value_to_bytes( int_value.clone(), @@ -1514,9 +1524,24 @@ fn bit_array<'a>( Ok(docvec!["codepointBits(", value, ")"]) } - // Bit strings + // Bit arrays [Opt::Bits { .. }] => Ok(docvec![value, ".buffer"]), + // Bit arrays with explicit size. The explicit size slices the bit array to the + // specified size. A runtime exception is thrown if the size exceeds the number + // of bits in the bit array. + [Opt::Bits { .. }, Opt::Size { value: size, .. }] + | [Opt::Size { value: size, .. }, Opt::Bits { .. }] => match &**size { + Constant::Int { value: size, .. } => { + Ok(docvec![value, ".slice(0, ", size, ")"]) + } + + _ => Err(Error::Unsupported { + feature: "This bit array segment option".into(), + location: segment.location, + }), + }, + // Anything else _ => Err(Error::Unsupported { feature: "This bit array segment option".into(), @@ -1578,15 +1603,6 @@ fn sized_bit_array_segment_details<'a>( _ => None, }; - if let Some(size_value) = size_value.as_ref() { - if *size_value > BigInt::ZERO && size_value % 8 != BigInt::ZERO { - return Err(Error::Unsupported { - feature: "Non byte aligned array".into(), - location: segment.location, - }); - } - } - (size_value, constant_expr_fun(tracker, size)?) } _ => { diff --git a/compiler-core/src/javascript/pattern.rs b/compiler-core/src/javascript/pattern.rs index 3c33729211f..1f59318edda 100644 --- a/compiler-core/src/javascript/pattern.rs +++ b/compiler-core/src/javascript/pattern.rs @@ -16,19 +16,18 @@ enum Index<'a> { Int(usize), String(&'a str), ByteAt(usize), - IntFromSlice { + BitArraySliceToInt { start: usize, end: usize, endianness: Endianness, is_signed: bool, }, - FloatFromSlice { + BitArraySliceToFloat { start: usize, end: usize, endianness: Endianness, }, - BinaryFromSlice(usize, usize), - SliceAfter(usize), + BitArraySlice(usize, Option), StringPrefixSlice(usize), } @@ -40,26 +39,37 @@ pub(crate) struct Generator<'module_ctx, 'expression_gen, 'a> { assignments: Vec>, } +#[derive(Debug)] +pub enum BitArrayTailSpreadType { + /// The tail of the bit array pattern is for all remaining bits + Bits, + + /// The tail of the bit array pattern is for all remaining whole bytes. This + /// requires an additional runtime check that the number of remaining bits + /// is a multiple of 8, as otherwise the pattern doesn't match. + Bytes, +} + struct Offset { - bytes: usize, - open_ended: bool, + bits: usize, + tail_spread_type: Option, } impl Offset { pub fn new() -> Self { Self { - bytes: 0, - open_ended: false, + bits: 0, + tail_spread_type: None, } } // This should never be called on an open ended offset // However previous checks ensure bit_array segments without a size are only // allowed at the end of a pattern pub fn increment(&mut self, step: usize) { - self.bytes += step + self.bits += step } - pub fn set_open_ended(&mut self) { - self.open_ended = true + pub fn set_open_ended(&mut self, tail_spread_type: BitArrayTailSpreadType) { + self.tail_spread_type = Some(tail_spread_type); } } @@ -106,14 +116,14 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' self.path.push(Index::ByteAt(i)); } - fn push_int_from_slice( + fn push_bit_array_slice_to_int( &mut self, start: usize, end: usize, endianness: Endianness, is_signed: bool, ) { - self.path.push(Index::IntFromSlice { + self.path.push(Index::BitArraySliceToInt { start, end, endianness, @@ -121,20 +131,16 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' }); } - fn push_float_from_slice(&mut self, start: usize, end: usize, endianness: Endianness) { - self.path.push(Index::FloatFromSlice { + fn push_bit_array_slice_to_float(&mut self, start: usize, end: usize, endianness: Endianness) { + self.path.push(Index::BitArraySliceToFloat { start, end, endianness, }); } - fn push_binary_from_slice(&mut self, start: usize, end: usize) { - self.path.push(Index::BinaryFromSlice(start, end)); - } - - fn push_rest_from(&mut self, i: usize) { - self.path.push(Index::SliceAfter(i)); + fn push_bit_array_slice(&mut self, start: usize, end: Option) { + self.path.push(Index::BitArraySlice(start, end)); } fn push_string_times(&mut self, s: &'a str, times: usize) { @@ -159,13 +165,13 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' // TODO: escape string if needed Index::String(s) => docvec!(".", maybe_escape_property_doc(s)), Index::ByteAt(i) => docvec!(".byteAt(", i, ")"), - Index::IntFromSlice { + Index::BitArraySliceToInt { start, end, endianness, is_signed, } => docvec!( - ".intFromSlice(", + ".sliceToInt(", start, ", ", end, @@ -175,12 +181,12 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' bool(*is_signed), ")" ), - Index::FloatFromSlice { + Index::BitArraySliceToFloat { start, end, endianness, } => docvec!( - ".floatFromSlice(", + ".sliceToFloat(", start, ", ", end, @@ -188,10 +194,10 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' bool(endianness.is_big()), ")" ), - Index::BinaryFromSlice(start, end) => { - docvec!(".binaryFromSlice(", start, ", ", end, ")") - } - Index::SliceAfter(i) => docvec!(".sliceAfter(", i, ")"), + Index::BitArraySlice(start, end) => match end { + Some(end) => docvec!(".slice(", start, ", ", end, ")"), + None => docvec!(".slice(", start, ")"), + }, Index::StringPrefixSlice(i) => docvec!(".slice(", i, ")"), })) } @@ -592,7 +598,9 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' match segment.value.as_ref() { Pattern::Int { int_value, .. } - if details.size <= SAFE_INT_SEGMENT_MAX_SIZE => + if details.size <= SAFE_INT_SEGMENT_MAX_SIZE + && details.size % 8 == 0 + && offset.bits % 8 == 0 => { let bytes = bit_array_segment_int_value_to_bytes( (*int_value).clone(), @@ -601,23 +609,26 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' )?; for byte in bytes { - self.push_byte_at(offset.bytes); + self.push_byte_at(offset.bits / 8); self.push_equality_check(subject.clone(), docvec![byte]); self.pop(); - offset.increment(1); + offset.increment(8); } } _ => { - let start = offset.bytes; - let increment = details.size / 8; - let end = offset.bytes + increment; + let start = offset.bits; + let increment = details.size; + let end = offset.bits + increment; if segment.type_ == crate::type_::int() { - if details.size == 8 && !details.is_signed { - self.push_byte_at(offset.bytes); + if details.size == 8 + && !details.is_signed + && offset.bits % 8 == 0 + { + self.push_byte_at(offset.bits / 8); } else { - self.push_int_from_slice( + self.push_bit_array_slice_to_int( start, end, details.endianness, @@ -625,7 +636,11 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' ); } } else { - self.push_float_from_slice(start, end, details.endianness); + self.push_bit_array_slice_to_float( + start, + end, + details.endianness, + ); } self.traverse_pattern(subject, &segment.value)?; @@ -635,25 +650,55 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' } } else { match segment.options.as_slice() { + [Opt::Bits { .. }] => { + self.push_bit_array_slice(offset.bits, None); + self.traverse_pattern(subject, &segment.value)?; + self.pop(); + offset.set_open_ended(BitArrayTailSpreadType::Bits); + Ok(()) + } + + [Opt::Bits { .. }, Opt::Size { value: size, .. }] + | [Opt::Size { value: size, .. }, Opt::Bits { .. }] => match &**size { + Pattern::Int { value, .. } => { + let start = offset.bits; + let increment = value.parse::().expect( + "part of an Int node should always parse as integer", + ); + offset.increment(increment); + let end = offset.bits; + + self.push_bit_array_slice(start, Some(end)); + self.traverse_pattern(subject, &segment.value)?; + self.pop(); + Ok(()) + } + + _ => Err(Error::Unsupported { + feature: "This bit array size option in patterns".into(), + location: segment.location, + }), + }, + [Opt::Bytes { .. }] => { - self.push_rest_from(offset.bytes); + self.push_bit_array_slice(offset.bits, None); self.traverse_pattern(subject, &segment.value)?; self.pop(); - offset.set_open_ended(); + offset.set_open_ended(BitArrayTailSpreadType::Bytes); Ok(()) } [Opt::Bytes { .. }, Opt::Size { value: size, .. }] | [Opt::Size { value: size, .. }, Opt::Bytes { .. }] => match &**size { Pattern::Int { value, .. } => { - let start = offset.bytes; + let start = offset.bits; let increment = value.parse::().expect( "part of an Int node should always parse as integer", - ); + ) * 8; offset.increment(increment); - let end = offset.bytes; + let end = offset.bits; - self.push_binary_from_slice(start, end); + self.push_bit_array_slice(start, Some(end)); self.traverse_pattern(subject, &segment.value)?; self.pop(); Ok(()) @@ -668,13 +713,19 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' [Opt::Utf8 { .. }] => match segment.value.as_ref() { Pattern::String { value, .. } => { for byte in value.as_bytes() { - self.push_byte_at(offset.bytes); - self.push_equality_check( - subject.clone(), - EcoString::from(format!("0x{byte:X}")).to_doc(), - ); + if offset.bits % 8 == 0 { + self.push_byte_at(offset.bits / 8); + } else { + self.push_bit_array_slice_to_int( + offset.bits, + offset.bits + 8, + Endianness::Big, + false, + ); + } + self.push_equality_check(subject.clone(), byte.to_doc()); self.pop(); - offset.increment(1); + offset.increment(8); } Ok(()) @@ -694,7 +745,11 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' } } - self.push_bit_array_length_check(subject.clone(), offset.bytes, offset.open_ended); + self.push_bit_array_bit_size_check( + subject.clone(), + offset.bits, + offset.tail_spread_type, + ); Ok(()) } Pattern::VarUsage { location, .. } => Err(Error::Unsupported { @@ -765,14 +820,6 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' }); } - // Ints that aren't byte-aligned are not supported - if segment.type_ == crate::type_::int() && size % 8 != 0 { - return Err(Error::Unsupported { - feature: "Non byte aligned integer in patterns".into(), - location: segment.location, - }); - } - let is_signed = segment .options .iter() @@ -850,15 +897,15 @@ impl<'module_ctx, 'expression_gen, 'a> Generator<'module_ctx, 'expression_gen, ' }) } - fn push_bit_array_length_check( + fn push_bit_array_bit_size_check( &mut self, subject: Document<'a>, - expected_bytes: usize, - has_tail_spread: bool, + expected_bit_size: usize, + tail_spread_type: Option, ) { - self.checks.push(Check::BitArrayLength { - expected_bytes, - has_tail_spread, + self.checks.push(Check::BitArrayBitSize { + expected_bit_size, + tail_spread_type, subject, path: self.path_document(), }) @@ -914,11 +961,11 @@ pub enum Check<'a> { expected_length: usize, has_tail_spread: bool, }, - BitArrayLength { + BitArrayBitSize { subject: Document<'a>, path: Document<'a>, - expected_bytes: usize, - has_tail_spread: bool, + expected_bit_size: usize, + tail_spread_type: Option, }, StringPrefix { subject: Document<'a>, @@ -1004,21 +1051,41 @@ impl<'a> Check<'a> { docvec!["!", subject, path, length_check,] } } - Check::BitArrayLength { + Check::BitArrayBitSize { subject, path, - expected_bytes, - has_tail_spread, + expected_bit_size, + tail_spread_type, } => { - let length_check = if has_tail_spread { - eco_format!(".length >= {expected_bytes}").to_doc() - } else { - eco_format!(".length == {expected_bytes}").to_doc() + let bit_size = docvec![subject.clone(), path.clone(), ".bitSize"]; + + let bit_size_check = match tail_spread_type { + Some(BitArrayTailSpreadType::Bits) => { + docvec![bit_size, " >= ", expected_bit_size] + } + Some(BitArrayTailSpreadType::Bytes) => { + // When the tail spread is for bytes rather than bits, + // check that there is a whole number of bytes left in + // the bit array + docvec![ + "(", + bit_size.clone(), + " >= ", + expected_bit_size, + " && (", + bit_size, + " - ", + expected_bit_size, + ") % 8 === 0)" + ] + } + None => docvec![bit_size, " == ", expected_bit_size], }; + if match_desired { - docvec![subject, path, length_check,] + bit_size_check } else { - docvec!["!(", subject, path, length_check, ")",] + docvec!["!(", bit_size_check, ")"] } } Check::StringPrefix { @@ -1042,7 +1109,7 @@ impl<'a> Check<'a> { | Check::Variant { .. } | Check::Equal { .. } | Check::ListLength { .. } - | Check::BitArrayLength { .. } + | Check::BitArrayBitSize { .. } | Check::StringPrefix { .. } | Check::Booly { .. } => false, Check::Guard { .. } => true, diff --git a/compiler-core/src/javascript/tests/bit_arrays.rs b/compiler-core/src/javascript/tests/bit_arrays.rs index eb99339bdf2..36117ac6c85 100644 --- a/compiler-core/src/javascript/tests/bit_arrays.rs +++ b/compiler-core/src/javascript/tests/bit_arrays.rs @@ -1,4 +1,10 @@ -use crate::{assert_js, assert_js_error, assert_ts_def}; +use hexpm::version::Version; +use pubgrub::range::Range; + +use crate::{ + assert_js, assert_js_error, assert_js_no_warnings_with_gleam_version, + assert_js_warnings_with_gleam_version, assert_ts_def, +}; #[test] fn empty() { @@ -309,11 +315,23 @@ fn go(x) { } #[test] -fn bits() { +fn bit_array_sliced() { assert_js!( r#" fn go(x) { - <> + <<<<0xAB>>:bits-4>> +} +"#, + ); +} + +#[test] +fn bit_array_dynamic_slice() { + assert_js!( + r#" +fn go(x) { + let i = 4 + <<<<0xAB>>:bits-size(i)>> } "#, ); @@ -374,6 +392,17 @@ fn go(x) { ); } +#[test] +fn match_sized_unaligned() { + assert_js!( + r#" +fn go(x) { + let assert <> = x +} +"#, + ); +} + #[test] fn match_sized_constant_pattern() { assert_js!( @@ -574,11 +603,12 @@ fn go(x) { } #[test] -fn match_non_byte_aligned_size_error() { +fn match_dynamic_bits_size_error() { assert_js_error!( r#" fn go(x) { - let assert <> = x + let n = 16 + let assert <> = x } "# ); @@ -696,18 +726,29 @@ fn go(x) { } #[test] -fn match_rest() { +fn match_bytes_with_size() { assert_js!( r#" fn go(x) { - let assert <<_, b:bytes>> = <<1,2,3>> + let assert <> = <<1, 2>> +} +"#, + ); +} + +#[test] +fn match_bits_with_size() { + assert_js!( + r#" +fn go(x) { + let assert <<_:4, f:bits-2, _:1>> = <<0x77:7>> } "#, ); } #[test] -fn match_rest_deprecated() { +fn match_rest_bytes() { assert_js!( r#" fn go(x) { @@ -717,6 +758,28 @@ fn go(x) { ); } +#[test] +fn match_rest_bits() { + assert_js!( + r#" +fn go(x) { + let assert <<_, b:bits>> = <<1,2,3>> +} +"#, + ); +} + +#[test] +fn match_rest_bits_unaligned() { + assert_js!( + r#" +fn go(x) { + let assert <<_:5, b:bits>> = <<1,2,3>> +} +"#, + ); +} + #[test] fn match_binary_size() { assert_js!( @@ -742,6 +805,7 @@ fn as_module_const() { "Gleam":utf8, 4.2:float, 4.2:32-float, + <<0xFA>>:bits-6, -1:64, << <<1, 2, 3>>:bits, @@ -775,72 +839,102 @@ fn go(x: Int) { ); } -// https://github.com/gleam-lang/gleam/issues/1591 #[test] -fn not_byte_aligned() { - assert_js_error!( +fn bit_array_literal_string_constant_is_treated_as_utf8() { + assert_js!(r#"const a = <<"hello", " ", "world">>"#); +} + +#[test] +fn bit_array_literal_string_is_treated_as_utf8() { + assert_js!( r#" -fn thing() { - 4 +pub fn main() { + <<"hello", " ", "world">> +}"# + ); } -fn go() { - <<256:4>> +#[test] +fn bit_array_literal_string_pattern_is_treated_as_utf8() { + assert_js!( + r#" +pub fn main() { + case <<>> { + <<"a", "b", _:bytes>> -> 1 + _ -> 2 + } +}"# + ); } -"#, + +#[test] +fn unaligned_int_expression_requires_v1_7() { + assert_js_warnings_with_gleam_version!( + Range::higher_than(Version::new(1, 6, 0)), + " +pub fn main() { + <<0:1>> +} + ", ); } #[test] -fn not_byte_aligned_explicit_sized() { - assert_js_error!( - r#" -fn go() { - <<256:size(4)>> +fn bits_expression_does_not_require_v1_7() { + assert_js_no_warnings_with_gleam_version!( + Range::higher_than(Version::new(1, 6, 0)), + " +pub fn main() { + <<<<0>>:bits>> } -"#, + ", ); } -// This test would ideally also result in go() being deleted like the previous tests -// but we can not know for sure what the value of a variable is going to be -// so right now go() is not deleted. #[test] -fn not_byte_aligned_variable() { - assert_js!( - r#" -fn go() { - let x = 4 - <<256:size(x)>> +fn sized_bits_expression_requires_v1_7() { + assert_js_warnings_with_gleam_version!( + Range::higher_than(Version::new(1, 6, 0)), + " +pub fn main() { + <<<<0>>:bits-5>> } -"#, + ", ); } #[test] -fn bit_array_literal_string_constant_is_treated_as_utf8() { - assert_js!(r#"const a = <<"hello", " ", "world">>"#); +fn unaligned_int_pattern_requires_v1_7() { + assert_js_warnings_with_gleam_version!( + Range::higher_than(Version::new(1, 6, 0)), + " +pub fn main() { + let assert <<_:3>> = <<0>> +} + ", + ); } #[test] -fn bit_array_literal_string_is_treated_as_utf8() { - assert_js!( - r#" +fn bits_pattern_requires_v1_7() { + assert_js_warnings_with_gleam_version!( + Range::higher_than(Version::new(1, 6, 0)), + " pub fn main() { - <<"hello", " ", "world">> -}"# + let assert <<_:bits>> = <<0>> +} + ", ); } #[test] -fn bit_array_literal_string_pattern_is_treated_as_utf8() { - assert_js!( - r#" +fn bytes_pattern_with_odd_size_does_not_require_v1_7() { + assert_js_no_warnings_with_gleam_version!( + Range::higher_than(Version::new(1, 6, 0)), + " pub fn main() { - case <<>> { - <<"a", "b", _:bytes>> -> 1 - _ -> 2 - } -}"# + let assert <<_:bytes-3>> = <<0, 1, 2>> +} + ", ); } diff --git a/compiler-core/src/javascript/tests/externals.rs b/compiler-core/src/javascript/tests/externals.rs index 7742c44ef98..10d17031fb8 100644 --- a/compiler-core/src/javascript/tests/externals.rs +++ b/compiler-core/src/javascript/tests/externals.rs @@ -1,4 +1,4 @@ -use crate::{assert_js, assert_js_error, assert_module_error, assert_ts_def}; +use crate::{assert_js, assert_module_error, assert_ts_def}; #[test] fn type_() { @@ -282,20 +282,6 @@ pub fn should_not_be_generated(x: Int) -> Int ); } -#[test] -fn erlang_bit_patterns() { - assert_js_error!( - r#" -pub fn should_not_be_generated(x) { - case x { - <<_, rest:bits>> -> rest - _ -> x - } -} -"# - ); -} - #[test] fn both_externals_no_valid_impl() { assert_js!( diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__as_module_const.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__as_module_const.snap index 534cb13749e..e11f0a22636 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__as_module_const.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__as_module_const.snap @@ -1,6 +1,6 @@ --- source: compiler-core/src/javascript/tests/bit_arrays.rs -expression: "\n pub const data = <<\n 0x1,\n 2,\n 2:size(16),\n 0x4:size(32),\n -1:32,\n \"Gleam\":utf8,\n 4.2:float,\n 4.2:32-float,\n -1:64,\n <<\n <<1, 2, 3>>:bits,\n \"Gleam\":utf8,\n 1024\n >>:bits\n >>\n " +expression: "\n pub const data = <<\n 0x1,\n 2,\n 2:size(16),\n 0x4:size(32),\n -1:32,\n \"Gleam\":utf8,\n 4.2:float,\n 4.2:32-float,\n <<0xFA>>:bits-6, \n -1:64,\n <<\n <<1, 2, 3>>:bits,\n \"Gleam\":utf8,\n 1024\n >>:bits\n >>\n " --- ----- SOURCE CODE @@ -13,6 +13,7 @@ expression: "\n pub const data = <<\n 0x1,\n 2,\n "Gleam":utf8, 4.2:float, 4.2:32-float, + <<0xFA>>:bits-6, -1:64, << <<1, 2, 3>>:bits, @@ -34,6 +35,7 @@ export const data = /* @__PURE__ */ toBitArray([ stringBits("Gleam"), sizedFloat(4.2, 64, true), sizedFloat(4.2, 32, true), + /* @__PURE__ */ toBitArray([250]).slice(0, 6), sizedInt(-1, 64, true), /* @__PURE__ */ toBitArray([ /* @__PURE__ */ toBitArray([1, 2, 3]).buffer, diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_dynamic_slice.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_dynamic_slice.snap new file mode 100644 index 00000000000..d874f6dd2df --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_dynamic_slice.snap @@ -0,0 +1,19 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n let i = 4\n <<<<0xAB>>:bits-size(i)>>\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + let i = 4 + <<<<0xAB>>:bits-size(i)>> +} + + +----- COMPILED JAVASCRIPT +import { toBitArray } from "../gleam.mjs"; + +function go(x) { + let i = 4; + return toBitArray([toBitArray([171]).slice(0, i)]); +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap index 77fc7a9e864..b76057866cf 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_literal_string_pattern_is_treated_as_utf8.snap @@ -16,7 +16,9 @@ import { toBitArray } from "../gleam.mjs"; export function main() { let $ = toBitArray([]); - if ($.byteAt(0) === 0x61 && $.byteAt(1) === 0x62 && $.length >= 2) { + if ($.byteAt(0) === 97 && + $.byteAt(1) === 98 && + ($.bitSize >= 16 && ($.bitSize - 16) % 8 === 0)) { return 1; } else { return 2; diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_sliced.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_sliced.snap new file mode 100644 index 00000000000..4b9c056ba0f --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_array_sliced.snap @@ -0,0 +1,17 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n <<<<0xAB>>:bits-4>>\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + <<<<0xAB>>:bits-4>> +} + + +----- COMPILED JAVASCRIPT +import { toBitArray } from "../gleam.mjs"; + +function go(x) { + return toBitArray([toBitArray([171]).slice(0, 4)]); +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_string.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_string.snap index 5c31e7b6174..21842539c68 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_string.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bit_string.snap @@ -13,5 +13,5 @@ fn go(x) { import { toBitArray } from "../gleam.mjs"; function go(x) { - return toBitArray([x.buffer]); + return toBitArray([x]); } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits.snap index 5c31e7b6174..21842539c68 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits.snap @@ -13,5 +13,5 @@ fn go(x) { import { toBitArray } from "../gleam.mjs"; function go(x) { - return toBitArray([x.buffer]); + return toBitArray([x]); } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_expression_requires_v1_7.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_expression_requires_v1_7.snap new file mode 100644 index 00000000000..e8826db9454 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_expression_requires_v1_7.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\npub fn main() {\n <<<<0>>:bits>>\n}\n " +--- +----- SOURCE CODE + +pub fn main() { + <<<<0>>:bits>> +} + + +----- WARNING +warning: Incompatible gleam version range + ┌─ /src/warning/wrn.gleam:3:11 + │ +3 │ <<<<0>>:bits>> + │ ^^^^ This requires a Gleam version >= 1.7.0 + +Use of unaligned bit arrays on the JavaScript target was introduced in +version v1.7.0. But the Gleam version range specified in your `gleam.toml` +would allow this code to run on an earlier version like v1.6.0, resulting +in compilation errors! +Hint: Remove the version constraint from your `gleam.toml` or update it to be: + + gleam = ">= 1.7.0" diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_pattern_requires_v1_7.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_pattern_requires_v1_7.snap new file mode 100644 index 00000000000..48f8004c831 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__bits_pattern_requires_v1_7.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\npub fn main() {\n let assert <<_:bits>> = <<0>>\n}\n " +--- +----- SOURCE CODE + +pub fn main() { + let assert <<_:bits>> = <<0>> +} + + +----- WARNING +warning: Incompatible gleam version range + ┌─ /src/warning/wrn.gleam:3:18 + │ +3 │ let assert <<_:bits>> = <<0>> + │ ^^^^ This requires a Gleam version >= 1.7.0 + +Use of unaligned bit arrays on the JavaScript target was introduced in +version v1.7.0. But the Gleam version range specified in your `gleam.toml` +would allow this code to run on an earlier version like v1.6.0, resulting +in compilation errors! +Hint: Remove the version constraint from your `gleam.toml` or update it to be: + + gleam = ">= 1.7.0" diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__discard_sized.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__discard_sized.snap index 1198c99532d..3b98e8034dc 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__discard_sized.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__discard_sized.snap @@ -14,7 +14,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 3)) { + if (!(x.bitSize == 24)) { throw makeError( "let_assert", "my/mod", @@ -24,7 +24,7 @@ function go(x) { { value: x } ) } - if (!(x.length == 3)) { + if (!(x.bitSize == 24)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__empty_match.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__empty_match.snap index 397f5046197..f857c03ea84 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__empty_match.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__empty_match.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 0)) { + if (!(x.bitSize == 0)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_binary_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_binary_size.snap index 88f2b82acb8..2c3a94dcd85 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_binary_size.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_binary_size.snap @@ -14,7 +14,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 3)) { + if (!(x.bitSize == 24)) { throw makeError( "let_assert", "my/mod", @@ -24,8 +24,8 @@ function go(x) { { value: x } ) } - let a = x.binaryFromSlice(1, 3); - if (!(x.length == 3)) { + let a = x.slice(8, 24); + if (!(x.bitSize == 24)) { throw makeError( "let_assert", "my/mod", @@ -35,6 +35,6 @@ function go(x) { { value: x } ) } - let b = x.binaryFromSlice(1, 3); + let b = x.slice(8, 24); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bits_with_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bits_with_size.snap new file mode 100644 index 00000000000..030761c5348 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bits_with_size.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n let assert <<_:4, f:bits-2, _:1>> = <<0x77:7>>\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + let assert <<_:4, f:bits-2, _:1>> = <<0x77:7>> +} + + +----- COMPILED JAVASCRIPT +import { makeError, toBitArray, sizedInt } from "../gleam.mjs"; + +function go(x) { + let $ = toBitArray([sizedInt(0x77, 7, true)]); + if (!($.bitSize == 7)) { + throw makeError( + "let_assert", + "my/mod", + 3, + "go", + "Pattern match failed, no pattern matched the value.", + { value: $ } + ) + } + let f = $.slice(4, 6); + return $; +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes.snap index e9b394c17d8..30181de522f 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 1 || !(x.length == 2)) { + if (x.byteAt(0) !== 1 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes_with_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes_with_size.snap new file mode 100644 index 00000000000..6fa594a44ce --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_bytes_with_size.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n let assert <> = <<1, 2>>\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + let assert <> = <<1, 2>> +} + + +----- COMPILED JAVASCRIPT +import { makeError, toBitArray } from "../gleam.mjs"; + +function go(x) { + let $ = toBitArray([1, 2]); + if (!($.bitSize == 16)) { + throw makeError( + "let_assert", + "my/mod", + 3, + "go", + "Pattern match failed, no pattern matched the value.", + { value: $ } + ) + } + let f = $.slice(0, 16); + return $; +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size_error.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size_error.snap new file mode 100644 index 00000000000..4bd76059845 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bits_size_error.snap @@ -0,0 +1,20 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n let n = 16\n let assert <> = x\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + let n = 16 + let assert <> = x +} + + +----- ERROR +error: Unsupported feature for compilation target + ┌─ /src/javascript/error.gleam:4:16 + │ +4 │ let assert <> = x + │ ^^^^^^^^^^^^^^ + +This bit array size option in patterns is not supported for JavaScript compilation. diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float.snap index 5de87f67307..84e068236c5 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 9)) { + if (!(x.bitSize == 72)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.floatFromSlice(0, 8, true); + let a = x.sliceToFloat(0, 64, true); let b = x.byteAt(8); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_big_endian.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_big_endian.snap index b492bb6431a..b7788ce7cab 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_big_endian.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_big_endian.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 9)) { + if (!(x.bitSize == 72)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.floatFromSlice(0, 8, true); + let a = x.sliceToFloat(0, 64, true); let b = x.byteAt(8); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_little_endian.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_little_endian.snap index 69d0a21f87a..fc1f5c6dd74 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_little_endian.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_little_endian.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 9)) { + if (!(x.bitSize == 72)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.floatFromSlice(0, 8, false); + let a = x.sliceToFloat(0, 64, false); let b = x.byteAt(8); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized.snap index f711dff2e86..1af61a249db 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 5)) { + if (!(x.bitSize == 40)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.floatFromSlice(0, 4, true); + let a = x.sliceToFloat(0, 32, true); let b = x.byteAt(4); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_big_endian.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_big_endian.snap index 23f556b4d68..ea91b34ccb9 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_big_endian.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_big_endian.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 5)) { + if (!(x.bitSize == 40)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.floatFromSlice(0, 4, true); + let a = x.sliceToFloat(0, 32, true); let b = x.byteAt(4); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_little_endian.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_little_endian.snap index 2ed2ceed95b..8059c494d4d 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_little_endian.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_float_sized_little_endian.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 5)) { + if (!(x.bitSize == 40)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.floatFromSlice(0, 4, false); + let a = x.sliceToFloat(0, 32, false); let b = x.byteAt(4); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_non_byte_aligned_size_error.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_non_byte_aligned_size_error.snap deleted file mode 100644 index 6ed1bbddfde..00000000000 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_non_byte_aligned_size_error.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: compiler-core/src/javascript/tests/bit_arrays.rs -expression: "\nfn go(x) {\n let assert <> = x\n}\n" ---- ------ SOURCE CODE - -fn go(x) { - let assert <> = x -} - - ------ ERROR -error: Unsupported feature for compilation target - ┌─ /src/javascript/error.gleam:3:16 - │ -3 │ let assert <> = x - │ ^^^^^^^^^ - -Non byte aligned integer in patterns is not supported for JavaScript compilation. diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_deprecated.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits.snap similarity index 71% rename from compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_deprecated.snap rename to compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits.snap index 52852c76e1f..e665be65224 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_deprecated.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits.snap @@ -1,11 +1,11 @@ --- source: compiler-core/src/javascript/tests/bit_arrays.rs -expression: "\nfn go(x) {\n let assert <<_, b:bytes>> = <<1,2,3>>\n}\n" +expression: "\nfn go(x) {\n let assert <<_, b:bits>> = <<1,2,3>>\n}\n" --- ----- SOURCE CODE fn go(x) { - let assert <<_, b:bytes>> = <<1,2,3>> + let assert <<_, b:bits>> = <<1,2,3>> } @@ -14,7 +14,7 @@ import { makeError, toBitArray } from "../gleam.mjs"; function go(x) { let $ = toBitArray([1, 2, 3]); - if (!($.length >= 1)) { + if (!($.bitSize >= 8)) { throw makeError( "let_assert", "my/mod", @@ -24,6 +24,6 @@ function go(x) { { value: $ } ) } - let b = $.sliceAfter(1); + let b = $.slice(8); return $; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits_unaligned.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits_unaligned.snap new file mode 100644 index 00000000000..b50e2889fcf --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bits_unaligned.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n let assert <<_:5, b:bits>> = <<1,2,3>>\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + let assert <<_:5, b:bits>> = <<1,2,3>> +} + + +----- COMPILED JAVASCRIPT +import { makeError, toBitArray } from "../gleam.mjs"; + +function go(x) { + let $ = toBitArray([1, 2, 3]); + if (!($.bitSize >= 5)) { + throw makeError( + "let_assert", + "my/mod", + 3, + "go", + "Pattern match failed, no pattern matched the value.", + { value: $ } + ) + } + let b = $.slice(5); + return $; +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bytes.snap similarity index 86% rename from compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest.snap rename to compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bytes.snap index 52852c76e1f..df0676a15f9 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_rest_bytes.snap @@ -14,7 +14,7 @@ import { makeError, toBitArray } from "../gleam.mjs"; function go(x) { let $ = toBitArray([1, 2, 3]); - if (!($.length >= 1)) { + if (!(($.bitSize >= 8 && ($.bitSize - 8) % 8 === 0))) { throw makeError( "let_assert", "my/mod", @@ -24,6 +24,6 @@ function go(x) { { value: $ } ) } - let b = $.sliceAfter(1); + let b = $.slice(8); return $; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed.snap index dd2bcb2277b..4496ab86f75 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 1)) { + if (!(x.bitSize == 8)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 1, true, true); + let a = x.sliceToInt(0, 8, true, true); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed_constant_pattern.snap index c9b7f4ddb32..ab58f9d25d3 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_signed_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 255 || !(x.length == 1)) { + if (x.byteAt(0) !== 255 || !(x.bitSize == 8)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized.snap index 9a6d55e94cc..672c6383f00 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 3)) { + if (!(x.bitSize == 24)) { throw makeError( "let_assert", "my/mod", @@ -23,7 +23,7 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, true, false); + let a = x.sliceToInt(0, 16, true, false); let b = x.byteAt(2); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian.snap index e34dea32041..b74e704ec20 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, true, false); + let a = x.sliceToInt(0, 16, true, false); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_constant_pattern.snap index dda77dd2f4e..fd338ee3197 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || !(x.length == 2)) { + if (x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed.snap index 7304a635800..a3721d0447c 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, true, true); + let a = x.sliceToInt(0, 16, true, true); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed_constant_pattern.snap index 04fde27a761..a4da91903e3 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_signed_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || !(x.length == 2)) { + if (x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned.snap index 2f846cce409..cc16bbdba97 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, true, false); + let a = x.sliceToInt(0, 16, true, false); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned_constant_pattern.snap index aee4e012205..4ad38c792f1 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_big_endian_unsigned_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || !(x.length == 2)) { + if (x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_constant_pattern.snap index 5d1c9084389..34d2dd944d1 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_constant_pattern.snap @@ -17,7 +17,7 @@ function go(x) { x.byteAt(0) !== 4 || x.byteAt(1) !== 210 || x.byteAt(2) !== 123 || - !(x.length == 3) + !(x.bitSize == 24) ) { throw makeError( "let_assert", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian.snap index 3e4b2095cc4..23b7b248faf 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, false, false); + let a = x.sliceToInt(0, 16, false, false); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_constant_pattern.snap index 816afd1dcc1..81f296db6dc 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 210 || x.byteAt(1) !== 4 || !(x.length == 2)) { + if (x.byteAt(0) !== 210 || x.byteAt(1) !== 4 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed.snap index b4fbced8316..d490255f9a6 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, false, true); + let a = x.sliceToInt(0, 16, false, true); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed_constant_pattern.snap index 21aa4f5218a..04753c2d43b 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_signed_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 210 || x.byteAt(1) !== 4 || !(x.length == 2)) { + if (x.byteAt(0) !== 210 || x.byteAt(1) !== 4 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned.snap index 5f43c02847d..6c1f85715ef 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let a = x.intFromSlice(0, 2, false, false); + let a = x.sliceToInt(0, 16, false, false); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned_constant_pattern.snap index 0b8353667ae..c91339b93d5 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_little_endian_unsigned_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 210 || x.byteAt(1) !== 4 || !(x.length == 2)) { + if (x.byteAt(0) !== 210 || x.byteAt(1) !== 4 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_unaligned.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_unaligned.snap new file mode 100644 index 00000000000..f3b1b707726 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_unaligned.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\nfn go(x) {\n let assert <> = x\n}\n" +--- +----- SOURCE CODE + +fn go(x) { + let assert <> = x +} + + +----- COMPILED JAVASCRIPT +import { makeError } from "../gleam.mjs"; + +function go(x) { + if (!(x.bitSize == 24)) { + throw makeError( + "let_assert", + "my/mod", + 3, + "go", + "Pattern match failed, no pattern matched the value.", + { value: x } + ) + } + let a = x.sliceToInt(0, 17, true, false); + let b = x.sliceToInt(17, 24, true, false); + return x; +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value.snap index 084b827ecb3..a1507329fa2 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 2)) { + if (!(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", @@ -23,6 +23,6 @@ function go(x) { { value: x } ) } - let i = x.intFromSlice(0, 2, true, false); + let i = x.sliceToInt(0, 16, true, false); return x; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value_constant_pattern.snap index b2461d25a18..0ec08359219 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_sized_value_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 1 || x.byteAt(1) !== 2 || !(x.length == 2)) { + if (x.byteAt(0) !== 1 || x.byteAt(1) !== 2 || !(x.bitSize == 16)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned.snap index ec4c7692d18..d63b3401b1c 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (!(x.length == 1)) { + if (!(x.bitSize == 8)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned_constant_pattern.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned_constant_pattern.snap index fe3dce00840..7a2f64a0178 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned_constant_pattern.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_unsigned_constant_pattern.snap @@ -13,7 +13,7 @@ fn go(x) { import { makeError } from "../gleam.mjs"; function go(x) { - if (x.byteAt(0) !== 254 || !(x.length == 1)) { + if (x.byteAt(0) !== 254 || !(x.bitSize == 8)) { throw makeError( "let_assert", "my/mod", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_utf8.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_utf8.snap index 8bd5c585a8b..62d4945763f 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_utf8.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_utf8.snap @@ -14,17 +14,17 @@ import { makeError } from "../gleam.mjs"; function go(x) { if ( - x.byteAt(0) !== 0x47 || - x.byteAt(1) !== 0x6C || - x.byteAt(2) !== 0x65 || - x.byteAt(3) !== 0x61 || - x.byteAt(4) !== 0x6D || - x.byteAt(5) !== 0x20 || - x.byteAt(6) !== 0xF0 || - x.byteAt(7) !== 0x9F || - x.byteAt(8) !== 0x91 || - x.byteAt(9) !== 0x8D || - !(x.length == 10) + x.byteAt(0) !== 71 || + x.byteAt(1) !== 108 || + x.byteAt(2) !== 101 || + x.byteAt(3) !== 97 || + x.byteAt(4) !== 109 || + x.byteAt(5) !== 32 || + x.byteAt(6) !== 240 || + x.byteAt(7) !== 159 || + x.byteAt(8) !== 145 || + x.byteAt(9) !== 141 || + !(x.bitSize == 80) ) { throw makeError( "let_assert", diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned.snap deleted file mode 100644 index 3875c595c17..00000000000 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: compiler-core/src/javascript/tests/bit_arrays.rs -expression: "\nfn thing() {\n 4\n}\n\nfn go() {\n <<256:4>>\n}\n" ---- ------ SOURCE CODE - -fn thing() { - 4 -} - -fn go() { - <<256:4>> -} - - ------ ERROR -error: Unsupported feature for compilation target - ┌─ /src/javascript/error.gleam:7:5 - │ -7 │ <<256:4>> - │ ^^^^^ - -Non byte aligned array is not supported for JavaScript compilation. diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_explicit_sized.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_explicit_sized.snap deleted file mode 100644 index 80ad3407c1d..00000000000 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_explicit_sized.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: compiler-core/src/javascript/tests/bit_arrays.rs -expression: "\nfn go() {\n <<256:size(4)>>\n}\n" ---- ------ SOURCE CODE - -fn go() { - <<256:size(4)>> -} - - ------ ERROR -error: Unsupported feature for compilation target - ┌─ /src/javascript/error.gleam:3:5 - │ -3 │ <<256:size(4)>> - │ ^^^^^^^^^^^ - -Non byte aligned array is not supported for JavaScript compilation. diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_variable.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_variable.snap deleted file mode 100644 index 635fbef566a..00000000000 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__not_byte_aligned_variable.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: compiler-core/src/javascript/tests/bit_arrays.rs -expression: "\nfn go() {\n let x = 4\n <<256:size(x)>>\n}\n" ---- ------ SOURCE CODE - -fn go() { - let x = 4 - <<256:size(x)>> -} - - ------ COMPILED JAVASCRIPT -import { toBitArray, sizedInt } from "../gleam.mjs"; - -function go() { - let x = 4; - return toBitArray([sizedInt(256, x, true)]); -} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__sized_bits_expression_requires_v1_7.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__sized_bits_expression_requires_v1_7.snap new file mode 100644 index 00000000000..0375466ab51 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__sized_bits_expression_requires_v1_7.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\npub fn main() {\n <<<<0>>:bits-5>>\n}\n " +--- +----- SOURCE CODE + +pub fn main() { + <<<<0>>:bits-5>> +} + + +----- WARNING +warning: Incompatible gleam version range + ┌─ /src/warning/wrn.gleam:3:16 + │ +3 │ <<<<0>>:bits-5>> + │ ^ This requires a Gleam version >= 1.7.0 + +Use of unaligned bit arrays on the JavaScript target was introduced in +version v1.7.0. But the Gleam version range specified in your `gleam.toml` +would allow this code to run on an earlier version like v1.6.0, resulting +in compilation errors! +Hint: Remove the version constraint from your `gleam.toml` or update it to be: + + gleam = ">= 1.7.0" diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_expression_requires_v1_7.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_expression_requires_v1_7.snap new file mode 100644 index 00000000000..18b8712e9da --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_expression_requires_v1_7.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\npub fn main() {\n <<0:1>>\n}\n " +--- +----- SOURCE CODE + +pub fn main() { + <<0:1>> +} + + +----- WARNING +warning: Incompatible gleam version range + ┌─ /src/warning/wrn.gleam:3:7 + │ +3 │ <<0:1>> + │ ^ This requires a Gleam version >= 1.7.0 + +Use of unaligned bit arrays on the JavaScript target was introduced in +version v1.7.0. But the Gleam version range specified in your `gleam.toml` +would allow this code to run on an earlier version like v1.6.0, resulting +in compilation errors! +Hint: Remove the version constraint from your `gleam.toml` or update it to be: + + gleam = ">= 1.7.0" diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_pattern_requires_v1_7.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_pattern_requires_v1_7.snap new file mode 100644 index 00000000000..4fc5d27d178 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__unaligned_int_pattern_requires_v1_7.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/javascript/tests/bit_arrays.rs +expression: "\npub fn main() {\n let assert <<_:3>> = <<0>>\n}\n " +--- +----- SOURCE CODE + +pub fn main() { + let assert <<_:3>> = <<0>> +} + + +----- WARNING +warning: Incompatible gleam version range + ┌─ /src/warning/wrn.gleam:3:18 + │ +3 │ let assert <<_:3>> = <<0>> + │ ^ This requires a Gleam version >= 1.7.0 + +Use of unaligned bit arrays on the JavaScript target was introduced in +version v1.7.0. But the Gleam version range specified in your `gleam.toml` +would allow this code to run on an earlier version like v1.6.0, resulting +in compilation errors! +Hint: Remove the version constraint from your `gleam.toml` or update it to be: + + gleam = ">= 1.7.0" diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__erlang_bit_patterns.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__erlang_bit_patterns.snap deleted file mode 100644 index 68646b45bd5..00000000000 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__externals__erlang_bit_patterns.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -source: compiler-core/src/javascript/tests/externals.rs -expression: "\npub fn should_not_be_generated(x) {\n case x {\n <<_, rest:bits>> -> rest\n _ -> x\n }\n}\n" ---- ------ SOURCE CODE - -pub fn should_not_be_generated(x) { - case x { - <<_, rest:bits>> -> rest - _ -> x - } -} - - ------ ERROR -error: Unsupported feature for compilation target - ┌─ /src/javascript/error.gleam:4:10 - │ -4 │ <<_, rest:bits>> -> rest - │ ^^^^^^^^^ - -This bit array segment option in patterns is not supported for JavaScript compilation. diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs index f60fb25b474..5cdebe505b5 100644 --- a/compiler-core/src/type_/error.rs +++ b/compiler-core/src/type_/error.rs @@ -882,6 +882,7 @@ pub enum FeatureKind { RecordAccessVariantInference, LetAssertWithMessage, VariantWithDeprecatedAnnotation, + UnalignedBitArrayOnJavascript, } impl FeatureKind { @@ -904,9 +905,9 @@ impl FeatureKind { FeatureKind::RecordUpdateVariantInference | FeatureKind::RecordAccessVariantInference => Version::new(1, 6, 0), - FeatureKind::VariantWithDeprecatedAnnotation | FeatureKind::LetAssertWithMessage => { - Version::new(1, 7, 0) - } + FeatureKind::VariantWithDeprecatedAnnotation + | FeatureKind::LetAssertWithMessage + | FeatureKind::UnalignedBitArrayOnJavascript => Version::new(1, 7, 0), } } } diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index fd97d955825..2676fb50ae8 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -1169,6 +1169,38 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } })?; + // Track usage of the unaligned bit arrays feature on JavaScript so that + // warnings can be emitted if the Gleam version constraint is too low + if self.environment.target == Target::JavaScript { + for option in options.iter() { + if let BitArrayOption::::Size { + value, location, .. + } = option + { + let mut using_unaligned_bit_array = false; + + if type_ == int() { + match &(**value).as_int_literal() { + Some(size) if size % 8 != 0 => { + using_unaligned_bit_array = true; + } + _ => (), + } + } else if type_ == bits() { + using_unaligned_bit_array = true; + } + + if using_unaligned_bit_array { + self.track_feature_usage( + FeatureKind::UnalignedBitArrayOnJavascript, + *location, + ); + break; + } + } + } + } + unify(type_.clone(), value.type_()) .map_err(|e| convert_unify_error(e, value.location()))?; diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs index a91cac10dfd..ad73c266213 100644 --- a/compiler-core/src/type_/pattern.rs +++ b/compiler-core/src/type_/pattern.rs @@ -1,6 +1,7 @@ use hexpm::version::Version; use im::hashmap; use itertools::Itertools; +use num_bigint::BigInt; /// Type inference and checking of patterns used in case expressions /// and variables bindings. @@ -294,6 +295,41 @@ impl<'a, 'b> PatternTyper<'a, 'b> { location: error.location, })?; + // Track usage of the unaligned bit arrays feature on JavaScript so that + // warnings can be emitted if the Gleam version constraint is too low + if self.environment.target == Target::JavaScript { + for option in options.iter() { + match option { + // Use of the `bits` segment type + BitArrayOption::::Bits { location } => { + self.track_feature_usage( + FeatureKind::UnalignedBitArrayOnJavascript, + *location, + ); + } + + // Int segments that aren't a whole number of bytes + BitArrayOption::::Size { value, .. } if segment_type == int() => { + match &**value { + Pattern::<_>::Int { + location, + int_value, + .. + } if int_value % 8 != BigInt::ZERO => { + self.track_feature_usage( + FeatureKind::UnalignedBitArrayOnJavascript, + *location, + ); + } + _ => (), + } + } + + _ => (), + } + } + } + let type_ = { match value.deref() { Pattern::Variable { .. } if segment_type == string() => { diff --git a/compiler-core/src/type_/tests.rs b/compiler-core/src/type_/tests.rs index ea1ce080dcf..4ae7c325fb9 100644 --- a/compiler-core/src/type_/tests.rs +++ b/compiler-core/src/type_/tests.rs @@ -231,7 +231,7 @@ fn get_warnings( warnings.take().into_iter().collect_vec() } -fn get_printed_warnings( +pub(crate) fn get_printed_warnings( src: &str, deps: Vec>, target: Target, @@ -346,6 +346,34 @@ macro_rules! assert_warnings_with_gleam_version { }; } +#[macro_export] +macro_rules! assert_js_warnings_with_gleam_version { + ($gleam_version:expr, $src:expr$(,)?) => { + let warning = $crate::type_::tests::get_printed_warnings( + $src, + vec![], + crate::build::Target::JavaScript, + Some($gleam_version), + ); + assert!(!warning.is_empty()); + let output = format!("----- SOURCE CODE\n{}\n\n----- WARNING\n{}", $src, warning); + insta::assert_snapshot!(insta::internals::AutoName, output, $src); + }; +} + +#[macro_export] +macro_rules! assert_js_no_warnings_with_gleam_version { + ($gleam_version:expr, $src:expr$(,)?) => { + let warning = $crate::type_::tests::get_printed_warnings( + $src, + vec![], + crate::build::Target::JavaScript, + Some($gleam_version), + ); + assert!(warning.is_empty()); + }; +} + #[macro_export] macro_rules! assert_no_warnings { ($src:expr $(,)?) => { diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__javascript_external_module_with_at_requires_v1_2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__javascript_external_module_with_at_requires_v1_2.snap index 25bd0ebb230..3bc032f2150 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__javascript_external_module_with_at_requires_v1_2.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__javascript_external_module_with_at_requires_v1_2.snap @@ -15,7 +15,7 @@ warning: Incompatible gleam version range 2 │ @external(javascript, "module@module", "func") │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This requires a Gleam version >= 1.2.0 -The ability to have `@` in a Javascript module's name was introduced in +The ability to have `@` in a JavaScript module's name was introduced in version v1.2.0. But the Gleam version range specified in your `gleam.toml` would allow this code to run on an earlier version like v1.0.0, resulting in compilation errors! diff --git a/compiler-core/src/warning.rs b/compiler-core/src/warning.rs index 8b24f656323..2a1de75c809 100644 --- a/compiler-core/src/warning.rs +++ b/compiler-core/src/warning.rs @@ -1074,7 +1074,7 @@ See: https://tour.gleam.run/functions/pipelines/", } FeatureKind::InternalAnnotation => "The `@internal` annotation was", FeatureKind::AtInJavascriptModules => { - "The ability to have `@` in a Javascript module's name was" + "The ability to have `@` in a JavaScript module's name was" } FeatureKind::RecordUpdateVariantInference => { "Record updates for custom types when the variant is known was" @@ -1088,6 +1088,9 @@ See: https://tour.gleam.run/functions/pipelines/", FeatureKind::VariantWithDeprecatedAnnotation => { "Deprecating individual custom type variants was" } + FeatureKind::UnalignedBitArrayOnJavascript => { + "Use of unaligned bit arrays on the JavaScript target was" + } }; Diagnostic { diff --git a/compiler-core/templates/prelude.d.mts b/compiler-core/templates/prelude.d.mts index cd99d98e271..2a0623cfb3c 100644 --- a/compiler-core/templates/prelude.d.mts +++ b/compiler-core/templates/prelude.d.mts @@ -23,18 +23,33 @@ export class NonEmpty extends List {} export class BitArray { buffer: Uint8Array; - constructor(buffer: Uint8Array); + bitSize: number; + + constructor(buffer: Uint8Array, bitSize: number); + get length(): number; - byteAt(index: number): number; + equals(other: BitArray): boolean; + byteAt(index: number): number | undefined; + slice(start: number, end: number): BitArray; + sliceToFloat(start: number, end: number, isBigEndian: boolean): number; + sliceToInt(start: number, end: number, isBigEndian: boolean, isSigned: boolean): number; + + /** @deprecated */ floatFromSlice(index: number, end: number, isBigEndian: boolean): number; + + /** @deprecated */ intFromSlice( start: number, end: number, isBigEndian: boolean, isSigned: boolean ): number; + + /** @deprecated */ binaryFromSlice(start: number, end: number): BitArray; - sliceAfter(index: number): BitArray; + + /** @deprecated */ + sliceAfter(index: number): BitArray; } export class UtfCodepoint { @@ -49,21 +64,6 @@ export function sizedInt( isBigEndian: boolean ): Uint8Array; -export function byteArrayToInt( - byteArray: Uint8Array, - start: number, - end: number, - isBigEndian: boolean, - isSigned: boolean -): number; - -export function byteArrayToFloat( - byteArray: Uint8Array, - start: number, - end: number, - isBigEndian: boolean -): number; - export function stringBits(string: string): Uint8Array; export function codepointBits(codepoint: UtfCodepoint): Uint8Array; diff --git a/compiler-core/templates/prelude.mjs b/compiler-core/templates/prelude.mjs index 2fd975ffa59..e925822f5d8 100644 --- a/compiler-core/templates/prelude.mjs +++ b/compiler-core/templates/prelude.mjs @@ -91,185 +91,411 @@ export class NonEmpty extends List { } } +export class UtfCodepoint { + constructor(value) { + this.value = value; + } +} + +/** + * A bit array is a contiguous sequence of bits equivalent to Erlang's Binary + * type. It consists of `buffer: Uint8Array` and `bitSize: number` fields. + * + * When the bit size is not a multiple of 8 some of the low bits in the last + * byte of `buffer` are unused and their value is undefined. + * + * It is not valid for a bit array to have unused trailing bytes. At least one + * bit in the last byte of `buffer` must be used by the bit array. This is + * checked in the constructor. + * + * Examples: + * + * - Bit array with 3 bytes: `buffer: [0xAB, 0xCD, 0xEF], bitSize: 24`. + * - Bit array with 3 bits: `buffer: [0xE0], bitSize: 3`. + * - Bit array with 17 bits: `buffer: [0xAB, 0xCD, 0x80], bitSize: 17`. + */ export class BitArray { - constructor(buffer) { + /** + * Constructs a new bit array from a `Uint8Array` and an optional size in + * bits. If no bit size is specified then it is taken as `buffer.length * 8`, + * i.e. all bytes in the buffer are used. + * + * @param {Uint8Array} buffer + * @param {number} [bitSize] + */ + constructor(buffer, bitSize) { if (!(buffer instanceof Uint8Array)) { - throw "BitArray can only be constructed from a Uint8Array"; + throw globalThis.Error( + "BitArray can only be constructed from a Uint8Array", + ); + } + + // If no bit size is provided then assume there is a whole number of bytes, + // and if one is provided check that it is valid for the buffer's size + if (bitSize === undefined) { + bitSize = buffer.length * 8; + } else if (buffer.length !== Math.trunc((bitSize + 7) / 8)) { + throw globalThis.Error( + "BitArray bit size is invalid for the buffer's length", + ); } + this.buffer = buffer; + this.bitSize = bitSize; } - // @internal + /** + * @internal + * + * Compares two bit arrays. It isn't possible to compare their buffers + * directly as the content of any unused bits in the last byte is undefined + * and must be ignored when checking equality. + * + * @param {BitArray} other + * @returns {boolean} + */ + equals(other) { + if (this.bitSize !== other.bitSize) { + return false; + } + + // Compare any whole bytes + const wholeByteCount = Math.trunc(this.bitSize / 8); + for (let i = 0; i < wholeByteCount; i++) { + if (this.buffer[i] !== other.buffer[i]) { + return false; + } + } + + // Compare any trailing bits + const trailingBitsCount = this.bitSize % 8; + if (trailingBitsCount) { + const unusedLowBitCount = 8 - trailingBitsCount; + if ( + this.buffer[wholeByteCount] >> unusedLowBitCount !== + other.buffer[wholeByteCount] >> unusedLowBitCount + ) { + return false; + } + } + + return true; + } + + /** + * @internal + * + * Returns the length in bytes of this bit array's internal buffer. In most + * cases the `bitSize` value should be used instead in order to handle + * unaligned bit arrays. + * + * @returns {number} + */ get length() { return this.buffer.length; } - // @internal + /** + * @internal + * + * Returns the byte at the given index in this bit array's internal buffer, or + * `undefined` if the index is out of range. + * + * @returns {number | undefined} + */ byteAt(index) { return this.buffer[index]; } - // @internal - floatFromSlice(start, end, isBigEndian) { - return byteArrayToFloat(this.buffer, start, end, isBigEndian); - } + /** + * @internal + * + * Slices this bit array to produce a new bit array. If `end` is not supplied + * then all bits from `start` onward are returned. + * + * If the slice is out of bounds then an exception is thrown. + * + * @param {number} start The start offset of the slice in bits. + * @param {number} [end] The end offset of the slice in bits. If this is not + * specified then all bits from `start` onward are returned. + * @returns {BitArray} + */ + slice(start, end) { + end ??= this.bitSize; + + this.#validateBitRange(start, end); + + // Handle zero-length slices + if (start === end) { + return new BitArray(new Uint8Array()); + } - // @internal - intFromSlice(start, end, isBigEndian, isSigned) { - return byteArrayToInt(this.buffer, start, end, isBigEndian, isSigned); - } + // Handle slices that cover the whole bit array + if (start === 0 && end === this.bitSize) { + return this; + } - // @internal - binaryFromSlice(start, end) { - const buffer = new Uint8Array( - this.buffer.buffer, - this.buffer.byteOffset + start, - end - start - ); - return new BitArray(buffer); - } + const isStartByteAligned = start % 8 === 0; - // @internal - sliceAfter(index) { - const buffer = new Uint8Array( - this.buffer.buffer, - this.buffer.byteOffset + index - ); - return new BitArray(buffer); - } -} + // When start is byte-aligned the existing ArrayBuffer is reused, avoiding a + // copy + if (isStartByteAligned) { + const startIndex = Math.trunc(start / 8); + const endIndex = Math.trunc((end + 7) / 8); -export class UtfCodepoint { - constructor(value) { - this.value = value; - } -} + const buffer = new Uint8Array( + this.buffer.buffer, + this.buffer.byteOffset + startIndex, + endIndex - startIndex, + ); -// @internal -export function toBitArray(segments) { - if (segments.length === 0) { - return new BitArray(new Uint8Array()); - } + return new BitArray(buffer, end - start); + } - if (segments.length === 1) { - // When there is a single Uint8Array segment, pass it directly to the bit - // array constructor to avoid a copy - if (segments[0] instanceof Uint8Array) { - return new BitArray(segments[0]); + const size = end - start; + + const startIndex = Math.trunc(start / 8); + const endIndex = Math.trunc((end - 1) / 8); + + // Handle the case of the slice being completely contained in a single byte + if (startIndex === endIndex) { + const highBitsCount = start % 8; + + // Shift the value to the high bits + const byte = this.buffer[startIndex] << highBitsCount; + + return new BitArray(new Uint8Array([byte]), size); } - return new BitArray(new Uint8Array(segments)); - } + // The bit slice is unaligned and spans multiple bytes, so accumulate it + // into a new buffer - // Count the total number of bytes, and check if there are any Uint8Array - // segments - let bytes = 0; - let hasUint8ArraySegment = false; - for (const segment of segments) { - if (segment instanceof Uint8Array) { - bytes += segment.byteLength; - hasUint8ArraySegment = true; - } else { - bytes++; + const buffer = new Uint8Array(Math.trunc((size + 7) / 8)); + const highBitsCount = start % 8; + const lowBitsCount = 8 - highBitsCount; + + let byteIndex = startIndex; + + for (let i = 0; i <= buffer.byteLength; i++, byteIndex++) { + buffer[i] = + (this.buffer[byteIndex] << highBitsCount) | + (this.buffer[byteIndex + 1] >> lowBitsCount); } - } - // If there aren't any Uint8Array segments then pass the segments array - // directly to the Uint8Array constructor - if (!hasUint8ArraySegment) { - return new BitArray(new Uint8Array(segments)); + return new BitArray(buffer, size); } - // Copy the segments into a Uint8Array - let u8Array = new Uint8Array(bytes); - let cursor = 0; - for (let segment of segments) { - if (segment instanceof Uint8Array) { - u8Array.set(segment, cursor); - cursor += segment.byteLength; - } else { - u8Array[cursor] = segment; - cursor++; + /** + * Throws an exception if the given start and end values are out of bounds for + * this bit array. + * + * @param {number} start + * @param {number} end + */ + #validateBitRange(start, end) { + if ( + start < 0 || + start > this.bitSize || + end < start || + end > this.bitSize + ) { + const msg = + `Invalid bit array slice: start = ${start}, end = ${end}, ` + + `bit size = ${this.bitSize}`; + throw new globalThis.Error(msg); } } - return new BitArray(u8Array); -} + /** + * @internal + * + * Interprets a slice of this bit array as a floating point number, either + * 32-bit or 64-bit, with the specified endianness. + * + * The value of `end - start` must be exactly 32 or 64, otherwise an error + * will be thrown. + * + * @param {number} start The start offset of the slice in bits. + * @param {number} start The end offset of the slice in bits. + * @param {boolean} isBigEndian Whether the slice is encoded in big endian. + * @returns {number} + */ + sliceToFloat(start, end, isBigEndian) { + this.#validateBitRange(start, end); + + const bitSize = end - start; + + // Check size is valid + if (bitSize !== 32 && bitSize !== 64) { + const msg = + `Sized floats must be 32-bit or 64-bit on JavaScript, ` + + `got size of ${bitSize} bits`; + throw new globalThis.Error(msg); + } -// @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) { - return new Uint8Array(); - } - if (size % 8 != 0) { - const msg = `Bit arrays must be byte aligned on JavaScript, got size of ${size} bits`; - throw new globalThis.Error(msg); - } + const isStartByteAligned = start % 8 === 0; - const byteArray = new Uint8Array(size / 8); + // If the bit range is byte aligned then the float can be read directly out + // of the existing buffer + if (isStartByteAligned) { + const view = new DataView(this.buffer.buffer, this.buffer.byteOffset); - let byteModulus = 256; + if (bitSize === 64) { + return view.getFloat64(start / 8, !isBigEndian); + } else { + return view.getFloat32(start / 8, !isBigEndian); + } + } - // Convert negative number to two's complement representation - if (value < 0) { - let valueModulus; + // Copy the unaligned bits into a new bit array + const alignedBits = this.slice(start, end); - // For output sizes larger than 48 bits BigInt is needed in order to - // maintain accuracy - if (size <= 48) { - valueModulus = 2 ** size; + // Read the float out of the aligned buffer + const view = new DataView(alignedBits.buffer.buffer); + if (bitSize === 64) { + return view.getFloat64(0, !isBigEndian); } else { - valueModulus = 1n << BigInt(size); + return view.getFloat32(0, !isBigEndian); + } + } - value = BigInt(value); - byteModulus = BigInt(byteModulus); + /** + * @internal + * + * Interprets a slice of this bit array as a signed or unsigned integer with + * the specified endianness. + * + * @param {number} start The start offset of the slice in bits. + * @param {number} end The end offset of the slice in bits. + * @param {boolean} isBigEndian Whether the slice is encoded in big endian. + * @param {boolean} isSigned Whether to interpret the slice as signed two's + * complement. + * @returns {number} + */ + sliceToInt(start, end, isBigEndian, isSigned) { + this.#validateBitRange(start, end); + + if (start === end) { + return 0; } - value %= valueModulus; - value = valueModulus + value; - } + const isStartByteAligned = start % 8 === 0; + const isEndByteAligned = end % 8 === 0; + + // If the slice is byte-aligned then there is no need to handle unaligned + // slices, meaning a simpler and faster implementation can be used instead + if (isStartByteAligned && isEndByteAligned) { + return this.#intFromAlignedSlice( + start / 8, + end / 8, + isBigEndian, + isSigned, + ); + } + + const size = end - start; + + const startByteIndex = Math.trunc(start / 8); + const endByteIndex = Math.trunc((end - 1) / 8); - // The following loops work with both Number and BigInt types - if (isBigEndian) { - for (let i = byteArray.length - 1; i >= 0; i--) { - const byte = value % byteModulus; - byteArray[i] = Number(byte); - value = (value - byte) / byteModulus; + // Handle the case of the slice being completely contained in a single byte + if (startByteIndex == endByteIndex) { + const mask = 0xff >> start % 8; + const unusedLowBitCount = (8 - (end % 8)) % 8; + + let value = (this.buffer[startByteIndex] & mask) >> unusedLowBitCount; + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2 ** (size - 1); + if (value >= highBit) { + value -= highBit * 2; + } + } + + return value; } - } else { - for (let i = 0; i < byteArray.length; i++) { - const byte = value % byteModulus; - byteArray[i] = Number(byte); - value = (value - byte) / byteModulus; + + // The integer value to be read is not aligned and crosses at least one byte + // boundary in the input array + + if (size <= 53) { + return this.#intFromUnalignedSliceUsingNumber( + start, + end, + isBigEndian, + isSigned, + ); + } else { + return this.#intFromUnalignedSliceUsingBigInt( + start, + end, + isBigEndian, + isSigned, + ); } } - return byteArray; -} + /** + * Reads an aligned slice of any size as an integer. + * + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ + #intFromAlignedSlice(start, end, isBigEndian, isSigned) { + const byteSize = end - start; + + if (byteSize <= 6) { + return this.#intFromAlignedSliceUsingNumber( + start, + end, + isBigEndian, + isSigned, + ); + } else { + return this.#intFromAlignedSliceUsingBigInt( + start, + end, + isBigEndian, + isSigned, + ); + } + } -// @internal -export function byteArrayToInt(byteArray, start, end, isBigEndian, isSigned) { - const byteSize = end - start; + /** + * Reads an aligned slice up to 48 bits in size as an integer. Uses the + * JavaScript `number` type internally. + * + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ + #intFromAlignedSliceUsingNumber(start, end, isBigEndian, isSigned) { + const byteSize = end - start; - // Ints wider than 48 bits are read using a BigInt, but narrower ones can - // be read with a JS number which is faster - if (byteSize <= 6) { let value = 0; - // Read bytes as an unsigned integer value + // Read bytes as an unsigned integer if (isBigEndian) { for (let i = start; i < end; i++) { - value = value * 256 + byteArray[i]; + value *= 256; + value += this.buffer[i]; } } else { for (let i = end - 1; i >= start; i--) { - value = value * 256 + byteArray[i]; + value *= 256; + value += this.buffer[i]; } } - // For signed integers, check if the high bit is set and if so then - // reinterpret as two's complement + // For signed integers, if the high bit is set reinterpret as two's + // complement if (isSigned) { const highBit = 2 ** (byteSize * 8 - 1); if (value >= highBit) { @@ -278,22 +504,38 @@ export function byteArrayToInt(byteArray, start, end, isBigEndian, isSigned) { } return value; - } else { + } + + /** + * Reads an aligned slice of any size as an integer. Uses the JavaScript + * `BigInt` type internally. + * + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ + #intFromAlignedSliceUsingBigInt(start, end, isBigEndian, isSigned) { + const byteSize = end - start; + let value = 0n; // Read bytes as an unsigned integer value if (isBigEndian) { for (let i = start; i < end; i++) { - value = (value << 8n) + BigInt(byteArray[i]); + value *= 256n; + value += BigInt(this.buffer[i]); } } else { for (let i = end - 1; i >= start; i--) { - value = (value << 8n) + BigInt(byteArray[i]); + value *= 256n; + value += BigInt(this.buffer[i]); } } - // For signed integers, check if the high bit is set and if so then - // reinterpret as two's complement + // For signed integers, if the high bit is set reinterpret as two's + // complement if (isSigned) { const highBit = 1n << BigInt(byteSize * 8 - 1); if (value >= highBit) { @@ -305,52 +547,623 @@ export function byteArrayToInt(byteArray, start, end, isBigEndian, isSigned) { // values outside JavaScript's safe integer range. return Number(value); } -} -// @internal -export function byteArrayToFloat(byteArray, start, end, isBigEndian) { - const view = new DataView(byteArray.buffer); + /** + * Reads an unaligned slice up to 53 bits in size as an integer. Uses the + * JavaScript `number` type internally. + * + * This function assumes that the slice crosses at least one byte boundary in + * the input. + * + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ + #intFromUnalignedSliceUsingNumber(start, end, isBigEndian, isSigned) { + const isStartByteAligned = start % 8 === 0; + + let size = end - start; + let byteIndex = Math.trunc(start / 8); - const byteSize = end - start; + let value = 0; - if (byteSize === 8) { - return view.getFloat64(start, !isBigEndian); - } else if (byteSize === 4) { - return view.getFloat32(start, !isBigEndian); - } else { - const msg = `Sized floats must be 32-bit or 64-bit on JavaScript, got size of ${byteSize * 8} bits`; - throw new globalThis.Error(msg); + if (isBigEndian) { + // Read any leading bits + if (!isStartByteAligned) { + const leadingBitsCount = 8 - (start % 8); + value = this.buffer[byteIndex++] & ((1 << leadingBitsCount) - 1); + size -= leadingBitsCount; + } + + // Read any whole bytes + while (size >= 8) { + value *= 256; + value += this.buffer[byteIndex++]; + size -= 8; + } + + // Read any trailing bits + if (size > 0) { + value *= 2 ** size; + value += this.buffer[byteIndex] >> (8 - size); + } + } else { + // For little endian, if the start is aligned then whole bytes can be read + // directly out of the input array, with the trailing bits handled at the + // end + if (isStartByteAligned) { + let size = end - start; + let scale = 1; + + // Read whole bytes + while (size >= 8) { + value += this.buffer[byteIndex++] * scale; + scale *= 256; + size -= 8; + } + + // Read trailing bits + value += (this.buffer[byteIndex] >> (8 - size)) * scale; + } else { + // Read little endian data where the start is not byte-aligned. This is + // done by reading whole bytes that cross a byte boundary in the input + // data, then reading any trailing bits. + + const highBitsCount = start % 8; + const lowBitsCount = 8 - highBitsCount; + + let size = end - start; + let scale = 1; + + // Extract whole bytes + while (size >= 8) { + const byte = + (this.buffer[byteIndex] << highBitsCount) | + (this.buffer[byteIndex + 1] >> lowBitsCount); + + value += (byte & 0xff) * scale; + + scale *= 256; + size -= 8; + byteIndex++; + } + + // Read any trailing bits. These trailing bits may cross a byte boundary + // in the input buffer. + if (size > 0) { + const lowBitsUsed = size - Math.max(0, size - lowBitsCount); + + let trailingByte = + (this.buffer[byteIndex] & ((1 << lowBitsCount) - 1)) >> + (lowBitsCount - lowBitsUsed); + + size -= lowBitsUsed; + + if (size > 0) { + trailingByte *= 2 ** size; + trailingByte += this.buffer[byteIndex + 1] >> (8 - size); + } + + value += trailingByte * scale; + } + } + } + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2 ** (end - start - 1); + if (value >= highBit) { + value -= highBit * 2; + } + } + + return value; } -} -// @internal -export function stringBits(string) { - return new TextEncoder().encode(string); + /** + * @internal + * + * Reads an unaligned slice of any size as an integer. Uses the JavaScript + * `BigInt` type internally. + * + * This function assumes that the slice crosses at least one byte boundary in + * the input. + * + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ + #intFromUnalignedSliceUsingBigInt(start, end, isBigEndian, isSigned) { + const isStartByteAligned = start % 8 === 0; + + let size = end - start; + let byteIndex = Math.trunc(start / 8); + + let value = 0n; + + if (isBigEndian) { + // Read any leading bits + if (!isStartByteAligned) { + const leadingBitsCount = 8 - (start % 8); + value = BigInt( + this.buffer[byteIndex++] & ((1 << leadingBitsCount) - 1), + ); + size -= leadingBitsCount; + } + + // Read any whole bytes + while (size >= 8) { + value *= 256n; + value += BigInt(this.buffer[byteIndex++]); + size -= 8; + } + + // Read any trailing bits + if (size > 0) { + value <<= BigInt(size); + value += BigInt(this.buffer[byteIndex] >> (8 - size)); + } + } else { + // For little endian, if the start is aligned then whole bytes can be read + // directly out of the input array, with the trailing bits handled at the + // end + if (isStartByteAligned) { + let size = end - start; + let shift = 0n; + + // Read whole bytes + while (size >= 8) { + value += BigInt(this.buffer[byteIndex++]) << shift; + shift += 8n; + size -= 8; + } + + // Read trailing bits + value += BigInt(this.buffer[byteIndex] >> (8 - size)) << shift; + } else { + // Read little endian data where the start is not byte-aligned. This is + // done by reading whole bytes that cross a byte boundary in the input + // data, then reading any trailing bits. + + const highBitsCount = start % 8; + const lowBitsCount = 8 - highBitsCount; + + let size = end - start; + let shift = 0n; + + // Extract whole bytes + while (size >= 8) { + const byte = + (this.buffer[byteIndex] << highBitsCount) | + (this.buffer[byteIndex + 1] >> lowBitsCount); + + value += BigInt(byte & 0xff) << shift; + + shift += 8n; + size -= 8; + byteIndex++; + } + + // Read any trailing bits. These trailing bits may cross a byte boundary + // in the input buffer. + if (size > 0) { + const lowBitsUsed = size - Math.max(0, size - lowBitsCount); + + let trailingByte = + (this.buffer[byteIndex] & ((1 << lowBitsCount) - 1)) >> + (lowBitsCount - lowBitsUsed); + + size -= lowBitsUsed; + + if (size > 0) { + trailingByte <<= size; + trailingByte += this.buffer[byteIndex + 1] >> (8 - size); + } + + value += BigInt(trailingByte) << shift; + } + } + } + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2n ** BigInt(end - start - 1); + if (value >= highBit) { + value -= highBit * 2n; + } + } + + // Convert the result into a JS number. This may cause quantizing/error on + // values outside JavaScript's safe integer range. + return Number(value); + } + + // + // Following are deprecated internal functions no longer used by the compiler + // but they are kept around just in case external JavaScript FFI code used + // them. In theory no code should have used these functions because they were + // marked internal. + // + + /** + * @internal + * @deprecated Use `BitArray.sliceToFloat()` instead. + * + * @param {number} index + * @param {number} end + * @param {boolean} isBigEndian + * @returns {number} + */ + floatFromSlice(index, end, isBigEndian) { + return this.sliceToFloat(index * 8, end * 8, isBigEndian); + } + + /** + * @internal + * @deprecated Use `BitArray.sliceToInt()` instead. + * + * @param {number} index + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ + intFromSlice(index, end, isBigEndian, isSigned) { + return this.sliceToInt(index * 8, end * 8, isBigEndian, isSigned); + } + + /** + * @internal + * @deprecated Use `BitArray.slice()` instead. + * + * @param {number} start + * @param {number} end + * @returns {BitArray} + */ + binaryFromSlice(start, end) { + return this.slice(start * 8, end * 8); + } + + /** + * @internal + * @deprecated Use `BitArray.slice()` instead. + * + * @param {number} index + * @returns {BitArray} + */ + sliceAfter(index) { + return this.slice(index * 8); + } } -// @internal -export function codepointBits(codepoint) { - return stringBits(String.fromCodePoint(codepoint.value)); +/** + * @internal + * + * Joins the given segments into a new bit array, tightly packing them together. + * Each segment must be one of the following types: + * + * - A `number`: A single byte value in the range 0-255. Values outside this + * range will be wrapped. + * - A `Uint8Array`: A sequence of byte values of any length. + * - A `BitArray`: A sequence of bits of any length, which may not be byte + * aligned. + * + * The bit size of the returned bit array will be the sum of the size in bits + * of the input segments. + * + * @param {(number | Uint8Array | BitArray)[]} segments + * @returns {BitArray} + */ +export function toBitArray(segments) { + if (segments.length === 0) { + return new BitArray(new Uint8Array()); + } + + if (segments.length === 1) { + const segment = segments[0]; + + // When there is a single BitArray segment it can be returned as-is + if (segment instanceof BitArray) { + return segment; + } + + // When there is a single Uint8Array segment, pass it directly to the bit + // array constructor to avoid a copy + if (segment instanceof Uint8Array) { + return new BitArray(segment); + } + + return new BitArray(new Uint8Array(/** @type {number[]} */ (segments))); + } + + // Count the total number of bits and check if all segments are numbers, i.e. + // single bytes + let bitSize = 0; + let areAllSegmentsNumbers = true; + for (const segment of segments) { + if (segment instanceof BitArray) { + bitSize += segment.bitSize; + areAllSegmentsNumbers = false; + } else if (segment instanceof Uint8Array) { + bitSize += segment.byteLength * 8; + areAllSegmentsNumbers = false; + } else { + bitSize += 8; + } + } + + // If all segments are numbers then pass the segments array directly to the + // Uint8Array constructor + if (areAllSegmentsNumbers) { + return new BitArray(new Uint8Array(/** @type {number[]} */ (segments))); + } + + // Pack the segments into a Uint8Array + const buffer = new Uint8Array(Math.trunc((bitSize + 7) / 8)); + + // The current write position in bits into the above array. Byte-aligned + // segments, i.e. when the cursor is a multiple of 8, are able to be processed + // faster due to being able to copy bytes directly. + let cursor = 0; + + for (let segment of segments) { + const isCursorByteAligned = cursor % 8 === 0; + + if (segment instanceof BitArray) { + if (isCursorByteAligned) { + buffer.set(segment.buffer, cursor / 8); + cursor += segment.bitSize; + + // Zero any unused bits in the last byte of the buffer. Their content is + // undefined and shouldn't be included in the output. + const trailingBitsCount = segment.bitSize % 8; + if (trailingBitsCount) { + const lastByteIndex = Math.trunc(cursor / 8); + buffer[lastByteIndex] >>= 8 - trailingBitsCount; + buffer[lastByteIndex] <<= 8 - trailingBitsCount; + } + } else { + appendUnalignedBits(segment.buffer, segment.bitSize); + } + } else if (segment instanceof Uint8Array) { + if (isCursorByteAligned) { + buffer.set(segment, cursor / 8); + cursor += segment.byteLength * 8; + } else { + appendUnalignedBits(segment, segment.byteLength * 8); + } + } else { + if (isCursorByteAligned) { + buffer[cursor / 8] = segment; + cursor += 8; + } else { + appendUnalignedBits(new Uint8Array([segment]), 8); + } + } + } + + function appendUnalignedBits(unalignedBits, size) { + if (size <= 0) { + return; + } + + const highBitsCount = cursor % 8; + const lowBitsCount = 8 - highBitsCount; + + let byteIndex = Math.trunc(cursor / 8); + + for (let byte of unalignedBits) { + // If this is a partial byte then zero out the trailing bits as their + // content is undefined and shouldn't be included in the output + if (size < 8) { + byte >>= 8 - size; + byte <<= 8 - size; + } + + // Copy the high bits of the input byte to the low bits of the current + // output byte + buffer[byteIndex] |= byte >> highBitsCount; + + let appendedBitsCount = size - Math.max(0, size - lowBitsCount); + size -= appendedBitsCount; + cursor += appendedBitsCount; + + if (size === 0) { + break; + } + + // Copy the low bits of the input byte to the high bits of the next output + // byte + buffer[++byteIndex] = byte << lowBitsCount; + appendedBitsCount = size - Math.max(0, size - highBitsCount); + size -= appendedBitsCount; + cursor += appendedBitsCount; + } + } + + return new BitArray(buffer, bitSize); } -// @internal -export function sizedFloat(float, size, isBigEndian) { +/** + * @internal + * + * Encodes a floating point value into a `Uint8Array`. This is used to create + * float segments that are part of bit array expressions. + * + * @param {number} value + * @param {number} size Size in bits of the encoded float. Must be 32 or 64. + * @param {boolean} isBigEndian Whether to encode as big/little endian. + * @returns {Uint8Array} + */ +export function sizedFloat(value, size, isBigEndian) { if (size !== 32 && size !== 64) { - const msg = `Sized floats must be 32-bit or 64-bit on JavaScript, got size of ${size} bits`; + const msg = + `Sized floats must be 32-bit or 64-bit on JavaScript, ` + + `got size of ${size} bits`; throw new globalThis.Error(msg); } - const byteArray = new Uint8Array(size / 8); + const buffer = new Uint8Array(size / 8); - const view = new DataView(byteArray.buffer); + const view = new DataView(buffer.buffer); if (size == 64) { - view.setFloat64(0, float, !isBigEndian); - } else if (size === 32) { - view.setFloat32(0, float, !isBigEndian); + view.setFloat64(0, value, !isBigEndian); + } else { + view.setFloat32(0, value, !isBigEndian); } - return byteArray; + return buffer; +} + +/** + * @internal + * + * Encodes an integer value into a `Uint8Array`, or a `BitArray` if the size in + * bits is not a multiple of 8. This is used to create integer segments used in + * bit array expressions. + * + * @param {number} value + * @param {number} size Size of the encoded integer in bits. + * @param {boolean} isBigEndian Whether to encode as big/little endian. + * @returns {Uint8Array | BitArray} + */ +export function sizedInt(value, size, isBigEndian) { + if (size <= 0) { + return new Uint8Array(); + } + + // Fast path when size is 8 bits. This relies on the rounding behavior of the + // Uint8Array constructor. + if (size === 8) { + return new Uint8Array([value]); + } + + // Fast path when size is less than 8 bits: shift the value up to the high + // bits + if (size < 8) { + value <<= 8 - size; + return new BitArray(new Uint8Array([value]), size); + } + + // Allocate output buffer + const buffer = new Uint8Array(Math.trunc((size + 7) / 8)); + + // The number of trailing bits in the final byte. Will be zero if the size is + // an exact number of bytes. + const trailingBitsCount = size % 8; + + // The number of unused bits in the final byte of the buffer + const unusedBitsCount = 8 - trailingBitsCount; + + // For output sizes not exceeding 32 bits the number type is used. For larger + // output sizes the BigInt type is needed. + // + // The code in each of these two paths must be kept in sync. + if (size <= 32) { + if (isBigEndian) { + let i = buffer.length - 1; + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i--] = (value << unusedBitsCount) & 0xff; + value >>= trailingBitsCount; + } + + for (; i >= 0; i--) { + buffer[i] = value; + value >>= 8; + } + } else { + let i = 0; + + const wholeByteCount = Math.trunc(size / 8); + for (; i < wholeByteCount; i++) { + buffer[i] = value; + value >>= 8; + } + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i] = value << unusedBitsCount; + } + } + } else { + const bigTrailingBitsCount = BigInt(trailingBitsCount); + const bigUnusedBitsCount = BigInt(unusedBitsCount); + + let bigValue = BigInt(value); + + if (isBigEndian) { + let i = buffer.length - 1; + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i--] = Number(bigValue << bigUnusedBitsCount); + bigValue >>= bigTrailingBitsCount; + } + + for (; i >= 0; i--) { + buffer[i] = Number(bigValue); + bigValue >>= 8n; + } + } else { + let i = 0; + + const wholeByteCount = Math.trunc(size / 8); + for (; i < wholeByteCount; i++) { + buffer[i] = Number(bigValue); + bigValue >>= 8n; + } + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i] = Number(bigValue << bigUnusedBitsCount); + } + } + } + + // Integers that aren't a whole number of bytes are returned as a BitArray so + // their size in bits is tracked + if (trailingBitsCount) { + return new BitArray(buffer, size); + } + + return buffer; +} + +/** @type {TextEncoder | undefined} */ +let utf8Encoder; + +/** + * @internal + * + * Returns the UTF-8 bytes for a string. + * + * @param {string} string + * @returns {Uint8Array} + */ +export function stringBits(string) { + utf8Encoder ??= new TextEncoder(); + return utf8Encoder.encode(string); +} + +/** + * @internal + * + * Returns the UTF-8 bytes for a single UTF codepoint. + * + * @param {UtfCodepoint} codepoint + * @returns {Uint8Array} + */ +export function codepointBits(codepoint) { + return stringBits(String.fromCodePoint(codepoint.value)); } export class Result extends CustomType { diff --git a/test/javascript_prelude/main.mjs b/test/javascript_prelude/main.mjs index e4261d93ebd..3d3ef1ebd9c 100755 --- a/test/javascript_prelude/main.mjs +++ b/test/javascript_prelude/main.mjs @@ -13,6 +13,7 @@ import { toBitArray, toList, sizedInt, + sizedFloat, } from "./prelude.mjs"; let failures = 0; @@ -134,20 +135,41 @@ assertNotEqual(List.fromArray([1, 2]), List.fromArray([1, 2, new Ok(2)])); assertNotEqual(List.fromArray([1]), List.fromArray([])); assertNotEqual(List.fromArray([]), List.fromArray([1])); -assertEqual(new BitArray(new Uint8Array([])), new BitArray(new Uint8Array([]))); +assertEqual(new UtfCodepoint(128013), new UtfCodepoint(128013)); +assertNotEqual(new UtfCodepoint(128013), new UtfCodepoint(128014)); + +// new BitArray() + assertEqual( new BitArray(new Uint8Array([1, 2, 3])), - new BitArray(new Uint8Array([1, 2, 3])), + new BitArray(new Uint8Array([1, 2, 3]), 24), ); -assertNotEqual( - new BitArray(new Uint8Array([1, 2])), - new BitArray(new Uint8Array([1, 2, 3])), + +assertThrows("`new BitArray()` throws with an ArrayBuffer", () => { + new BitArray(new ArrayBuffer(8)); +}); + +assertThrows( + "`new BitArray()` throws with a raw array", + () => new BitArray([1, 2]), ); -assertEqual(new UtfCodepoint(128013), new UtfCodepoint(128013)); -assertNotEqual(new UtfCodepoint(128013), new UtfCodepoint(128014)); +assertThrows( + "`new BitArray()` throws with invalid bit size", + () => new BitArray(new Uint8Array([1, 2]), 7), +); + +assertThrows( + "`new BitArray()` throws with invalid bit size", + () => new BitArray(new Uint8Array([1, 2]), 17), +); -// toBitArray +// toBitArray() + +assertEqual( + new BitArray(new Uint8Array([1, 2])), + toBitArray([new BitArray(new Uint8Array([1, 2]))]), +); assertEqual(new BitArray(new Uint8Array([])), toBitArray([])); @@ -179,17 +201,17 @@ for (const { input, u8 } of testValues) { assertEqual(new BitArray(new Uint8Array([])), toBitArray([new Uint8Array([])])); assertEqual( new BitArray(new Uint8Array([1, 2, 4, 8])), - toBitArray([new Uint8Array([1, 2, 4, 8])]) + toBitArray([new Uint8Array([1, 2, 4, 8])]), ); assertEqual( new BitArray(new Uint8Array(testValues.map((t) => t.u8))), - toBitArray(testValues.map((t) => t.input)) + toBitArray(testValues.map((t) => t.input)), ); assertEqual( new BitArray( - new Uint8Array([1, 2, 4, 8, ...testValues.map((t) => t.u8), 80, 90, 100]) + new Uint8Array([1, 2, 4, 8, ...testValues.map((t) => t.u8), 80, 90, 100]), ), toBitArray([ new Uint8Array([]), @@ -198,7 +220,7 @@ assertEqual( new Uint8Array([80, 90]), new Uint8Array([]), new Uint8Array([100]), - ]) + ]), ); assertEqual( @@ -216,6 +238,74 @@ assertEqual( toBitArray([codepointBits(new UtfCodepoint(128013))]), ); +assertEqual( + new BitArray(new Uint8Array([240, 159, 144, 141, 0xfe]), 39), + toBitArray([ + new BitArray(new Uint8Array([240, 159, 144])), + new BitArray(new Uint8Array([141])), + new BitArray(new Uint8Array([0xfe]), 7), + ]), +); + +assertEqual( + new BitArray(new Uint8Array([240, 159, 144, 0xa9, 0b11101010]), 39), + toBitArray([ + new BitArray(new Uint8Array([240, 159, 144])), + new BitArray(new Uint8Array([0xae]), 4), + new BitArray(new Uint8Array([0x9f]), 7), + new BitArray(new Uint8Array([0b01010000]), 4), + ]), +); + +assertEqual( + new BitArray( + new Uint8Array([ + 129, 145, 57, 255, 255, 255, 255, 255, 191, 157, 243, 182, 246, 62, 104, + 49, 62, 31, 110, 200, 120, 13, 88, + ]), + 181, + ), + toBitArray([ + sizedInt(2, 2, false), + sizedInt(0, 2, false), + sizedInt(200, 11, true), + sizedInt(-100, 49, false), + sizedFloat(-1.234, 32, true), + sizedFloat(-8.2e40, 64, false), + sizedInt(0xf, 5, false), + new Uint8Array([1]), + 0xab, + ]), +); + +// BitArray.equals() + +assertEqual(new BitArray(new Uint8Array([])), new BitArray(new Uint8Array([]))); +assertEqual( + new BitArray(new Uint8Array([1, 2, 3])), + new BitArray(new Uint8Array([1, 2, 3])), +); +assertNotEqual( + new BitArray(new Uint8Array([1, 2])), + new BitArray(new Uint8Array([1, 2, 3])), +); +assertNotEqual( + new BitArray(new Uint8Array([1, 0xf0]), 12), + new BitArray(new Uint8Array([2, 0xf0]), 12), +); +assertNotEqual( + new BitArray(new Uint8Array([1, 0xf0]), 12), + new BitArray(new Uint8Array([1, 0xf0]), 13), +); +assertNotEqual( + new BitArray(new Uint8Array([0x12, 0x30]), 12), + new BitArray(new Uint8Array([0x12, 0x4f]), 12), +); +assertEqual( + new BitArray(new Uint8Array([0x12, 0x30]), 12), + new BitArray(new Uint8Array([0x12, 0x3f]), 12), +); + // toList assertEqual(toList([]), List.fromArray([])); @@ -399,71 +489,125 @@ assertNotEqual(hasEqualsField, hasEqualsField2); 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); -assertEqual(new BitArray(new Uint8Array([160, 2, 3])).intFromSlice(0, 1, false, true), -96); -assertEqual(new BitArray(new Uint8Array([1, 2, 3])).intFromSlice(0, 2, true, false), 258); -assertEqual(new BitArray(new Uint8Array([1, 2, 3])).intFromSlice(0, 2, false, false), 513); -assertEqual(new BitArray(new Uint8Array([1, 160, 3])).intFromSlice(0, 2, false, true), -24575); -assertEqual(new BitArray(new Uint8Array([160, 2, 3])).intFromSlice(0, 2, true, false), 40962); -assertEqual(new BitArray(new Uint8Array([160, 2, 3])).intFromSlice(0, 2, true, true), -24574); -assertEqual( - new BitArray(new Uint8Array([255, 255, 255, 255, 255, 255, 255])).intFromSlice(0, 7, true, true), - -1, + +// BitArray.slice() + +assertThrows( + "`BitArray.slice()` throw if start is less than zero", + () => new BitArray(new Uint8Array([1])).slice(-1, 8), ); -assertEqual( - new BitArray(new Uint8Array([255, 255, 255, 255, 255, 255, 254])).intFromSlice(0, 7, true, false), - Number(0xFFFFFFFFFFFFFEn), + +assertThrows( + "`BitArray.slice()` throw if start is greater than the bit size", + () => new BitArray(new Uint8Array([1])).slice(9, 8), +); + +assertThrows( + "`BitArray.slice()` throw if end is before start", + () => new BitArray(new Uint8Array([1])).slice(4, 2), +); + +assertThrows( + "`BitArray.slice()` throw if end is greater than the bit size", + () => new BitArray(new Uint8Array([1])).slice(0, 10), ); + assertEqual( - new BitArray(new Uint8Array([63, 240, 0, 0, 0, 0, 0, 0])).floatFromSlice(0, 8, true), - 1.0, + new BitArray(new Uint8Array([1, 2, 3])).slice(0, 0), + new BitArray(new Uint8Array([])), ); + assertEqual( - new BitArray(new Uint8Array([0, 0, 0, 0, 0, 0, 240, 63])).floatFromSlice(0, 8, false), - 1.0, + new BitArray(new Uint8Array([0xbe, 0xff])).slice(0, 2), + new BitArray(new Uint8Array([0xbe]), 2), ); + assertEqual( - new BitArray(new Uint8Array([0xC9, 0x74, 0x24, 0x00])).floatFromSlice(0, 4, true), - -1000000.0, + new BitArray(new Uint8Array([0x12, 0b10101101, 0xff])).slice(10, 14), + new BitArray(new Uint8Array([0b10110100]), 4), ); + assertEqual( - new BitArray(new Uint8Array([0x00, 0x24, 0x74, 0xC9])).floatFromSlice(0, 4, false), - -1000000.0, + new BitArray(new Uint8Array([1, 2, 3])).slice(8, 24), + new BitArray(new Uint8Array([2, 3])), ); + assertEqual( - new BitArray(new Uint8Array([1, 2, 3, 4, 5])).binaryFromSlice(1, 4), + new BitArray(new Uint8Array([1, 2, 3, 4, 5])).slice(8, 32), new BitArray(new Uint8Array([2, 3, 4])), ); + assertEqual( - new BitArray(new Uint8Array([1, 2, 3])).sliceAfter(1), - new BitArray(new Uint8Array([2, 3])), + new BitArray(new Uint8Array([1, 0xfe, 0xa1])).slice(8, 19), + new BitArray(new Uint8Array([0xfe, 0xa1]), 11), ); -// sizedInt() +assertEqual( + new BitArray(new Uint8Array([17, 79, 190, 151, 98, 222, 101])).slice(19, 51), + new BitArray(new Uint8Array([244, 187, 22, 243]), 32), +); assertEqual( - sizedInt(100, 0, true), - new Uint8Array([]), + new BitArray(new Uint8Array([0, 0, 0, 0, 0xff, 0xe0])).slice(37, 41), + new BitArray(new Uint8Array([0b11111100]), 4), ); + assertEqual( - sizedInt(0, 32, true), - new Uint8Array([0, 0, 0, 0]), + new BitArray(new Uint8Array([0xaa, 0xbb, 0xff, 0, 0, 0])) + .slice(8, 48) + .floatFromSlice(1, 5, false), + 3.5733110840282835e-43, ); + +// sizedFloat() + +assertEqual(sizedFloat(16.25, 32, true), new Uint8Array([65, 130, 0, 0])); +assertEqual(sizedFloat(-16.25, 32, false), new Uint8Array([0, 0, 130, 193])); assertEqual( - sizedInt(1, 24, true), - new Uint8Array([0, 0, 1]), + sizedFloat(1000.5, 64, true), + new Uint8Array([64, 143, 68, 0, 0, 0, 0, 0]), ); assertEqual( - sizedInt(-1, 32, true), - new Uint8Array([255, 255, 255, 255]), + sizedFloat(-1000.5, 64, false), + new Uint8Array([0, 0, 0, 0, 0, 68, 143, 192]), ); + +// sizedInt() + +assertEqual(sizedInt(100, -8, true), new Uint8Array([])); +assertEqual(sizedInt(100, 0, true), new Uint8Array([])); +assertEqual(sizedInt(100, 8, true), new Uint8Array([100])); +assertEqual(sizedInt(-10, 8, true), new Uint8Array([246])); + +assertEqual(sizedInt(1, 1, true), new BitArray(new Uint8Array([0x80]), 1)); +assertEqual(sizedInt(2, 2, true), new BitArray(new Uint8Array([0x80]), 2)); +assertEqual(sizedInt(15, 4, true), new BitArray(new Uint8Array([0xf0]), 4)); +assertEqual(sizedInt(100, 7, true), new BitArray(new Uint8Array([200]), 7)); +assertEqual(sizedInt(-44, 7, true), new BitArray(new Uint8Array([168]), 7)); +assertEqual(sizedInt(-1, 6, true), new BitArray(new Uint8Array([252]), 6)); +assertEqual(sizedInt(-1231, 3, true), new BitArray(new Uint8Array([32]), 3)); +assertEqual(sizedInt(1231, 5, true), new BitArray(new Uint8Array([120]), 5)); + +assertEqual( + sizedInt(773, 10, true), + new BitArray(new Uint8Array([193, 64]), 10), +); +assertEqual( + sizedInt(-276, 10, false), + new BitArray(new Uint8Array([236, 128]), 10), +); +assertEqual(sizedInt(80000, 16, true), new Uint8Array([56, 128])); +assertEqual(sizedInt(-80000, 16, true), new Uint8Array([199, 128])); +assertEqual(sizedInt(1, 24, true), new Uint8Array([0, 0, 1])); assertEqual( - sizedInt(80000, 16, true), - new Uint8Array([56, 128]), + sizedInt(-100, 31, false), + new BitArray(new Uint8Array([156, 255, 255, 254]), 31), ); +assertEqual(sizedInt(0, 32, true), new Uint8Array([0, 0, 0, 0])); +assertEqual(sizedInt(-1, 32, true), new Uint8Array([255, 255, 255, 255])); assertEqual( - sizedInt(-80000, 16, true), - new Uint8Array([199, 128]), + sizedInt(-10, 33, true), + new BitArray(new Uint8Array([255, 255, 255, 251, 0]), 33), ); assertEqual( sizedInt(-489_391_639_457_909_760, 56, true), @@ -481,6 +625,190 @@ assertEqual( sizedInt(Number.MIN_SAFE_INTEGER, 64, true), new Uint8Array([255, 224, 0, 0, 0, 0, 0, 1]), ); +assertEqual( + sizedInt(Number.MAX_SAFE_INTEGER, 77, true), + new BitArray( + new Uint8Array([0, 0, 0, 255, 255, 255, 255, 255, 255, 248]), + 77, + ), +); +assertEqual( + sizedInt(Number.MIN_SAFE_INTEGER, 75, false), + new BitArray(new Uint8Array([1, 0, 0, 0, 0, 0, 224, 255, 255, 224]), 75), +); +assertEqual( + sizedInt(Number(9444732965739289353650176n), 75, true), + new BitArray(new Uint8Array([255, 255, 255, 255, 255, 248, 0, 0, 0, 0]), 75), +); + +// BitArray.sliceToFloat() + +function sliceToFloat(bytes, bitSize, start, end, isBigEndian) { + return new BitArray(new Uint8Array(bytes), bitSize).sliceToFloat( + start, + end, + isBigEndian, + ); +} + +assertEqual( + sliceToFloat([63, 240, 0, 0, 0, 0, 0, 0], 64, 0, 64, true), + 1.0, +); +assertEqual( + sliceToFloat([0, 0, 0, 0, 0, 0, 240, 63], 64, 0, 64, false), + 1.0, +); +assertEqual( + sliceToFloat([0xff, 0xc9, 0x74, 0x24, 0x00], 40, 8, 40, true), + -1000000.0, +); +assertEqual( + sliceToFloat([0x00, 0x24, 0x74, 0xc9], 32, 0, 32, false), + -1000000.0, +); +assertEqual( + sliceToFloat([112, 152, 127, 244, 0, 7, 192], 50, 11, 43, true), + -511.25, +); +assertEqual( + sliceToFloat([8, 0, 0, 0, 1, 129, 39, 103, 129, 254], 79, 7, 71, false), + -5011.75, +); + +// BitArray.sliceToInt() + +function testSliceToInt(bytes, bitSize, start, end, isBigEndian, isSigned) { + return new BitArray(new Uint8Array(bytes), bitSize).sliceToInt( + start, + end, + isBigEndian, + isSigned, + ); +} + +assertEqual(testSliceToInt([0b10011110], 8, 0, 4, true, false), 0b1001); +assertEqual(testSliceToInt([0b10011110], 8, 4, 8, true, false), 0b1110); +assertEqual(testSliceToInt([0b10011110], 8, 1, 6, true, true), 0b00111); +assertEqual(testSliceToInt([0b10011010], 8, 3, 8, true, true), -6); +assertEqual(testSliceToInt([0b10011010], 8, 0, 7, true, true), -51); +assertEqual( + testSliceToInt([0b11001100, 0b00100000], 11, 1, 11, false, false), + 408, +); +assertEqual(testSliceToInt([0xb6, 0xe3], 16, 0, 12, true, false), 0xb6e); +assertEqual(testSliceToInt([0xb6, 0xe3], 16, 0, 12, false, false), 0xeb6); +assertEqual( + testSliceToInt([0xff, 0xb6, 0xe3], 24, 8, 20, true, false), + 0xb6e, +); +assertEqual( + testSliceToInt([0xff, 0xb6, 0xe3], 24, 20, 24, true, false), + 0x03, +); +assertEqual( + testSliceToInt([0xff, 0xb6, 0xe3], 24, 8, 20, false, false), + 0xeb6, +); +assertEqual(testSliceToInt([0xa5, 0x6c, 0xaa], 24, 5, 18, true, false), 5554); +assertEqual( + testSliceToInt([0xa5, 0x6c, 0xaa], 24, 5, 18, false, true), + -3411, +); +assertEqual(testSliceToInt([1, 2, 3], 24, 0, 8, true, false), 1); +assertEqual(testSliceToInt([160, 2, 3], 24, 0, 8, false, true), -96); +assertEqual(testSliceToInt([1, 2, 3], 24, 0, 16, true, false), 258); +assertEqual(testSliceToInt([1, 2, 3], 24, 0, 16, false, false), 513); +assertEqual(testSliceToInt([1, 160, 3], 24, 0, 16, false, true), -24575); +assertEqual(testSliceToInt([160, 2, 3], 24, 0, 16, true, false), 40962); +assertEqual(testSliceToInt([3, 160, 2], 24, 8, 24, true, true), -24574); +assertEqual( + testSliceToInt([146, 192, 70, 25, 128], 33, 1, 24, true, true), + 1_228_870, +); +assertEqual(testSliceToInt([255, 255, 255, 255], 32, 0, 32, false, true), -1); +assertEqual( + testSliceToInt([217, 150, 209, 191, 0], 33, 1, 33, true, false), + 3_006_112_638, +); +assertEqual( + testSliceToInt([146, 192, 70, 25, 128], 33, 1, 33, true, true), + 629_181_491, +); +assertEqual( + testSliceToInt([251, 24, 47, 227, 128], 33, 1, 33, false, false), + 3_344_904_438, +); +assertEqual( + testSliceToInt([240, 102, 91, 101, 128], 33, 0, 33, false, false), + 5_995_456_240, +); +assertEqual( + testSliceToInt([231, 255, 255, 255, 254, 123], 48, 0, 40, true, true), + -103_079_215_106, +); +assertEqual( + testSliceToInt([0, 231, 255, 255, 253, 123, 17], 56, 1, 55, true, false), + 127_543_348_739_464, +); +assertEqual( + testSliceToInt([142, 231, 255, 255, 253, 123, 17, 139], 64, 8, 62, false, true), + -8425025061257241, +); +assertEqual( + testSliceToInt([142, 231, 255, 255, 253, 123, 17, 139], 64, 7, 62, false, true), + -8293899692933261, +); +assertEqual( + testSliceToInt([142, 231, 255, 255, 253, 123, 17], 56, 8, 48, true, true), + -103_079_215_749, +); +assertEqual( + testSliceToInt([255, 255, 255, 255, 255, 255, 255], 56, 0, 56, true, true), + -1, +); +assertEqual( + testSliceToInt([0x00, 0xaa, 255, 255, 255, 255, 255], 56, 0, 56, true, false), + 0xaaffffffffff, +); +assertEqual( + testSliceToInt([255, 255, 255, 255, 255, 0xaa, 0x00], 56, 0, 56, false, false), + 0xaaffffffffff, +); +assertEqual( + testSliceToInt([255, 255, 255, 255, 255, 255, 255], 56, 0, 56, true, false), + Number(0xfffffffffffffen), +); +assertEqual(testSliceToInt([0xfe, 0x3f], 16, 4, 12, true, false), 0xe3); +assertEqual(testSliceToInt([253, 94], 16, 3, 11, true, true), -22); +assertEqual(testSliceToInt([233, 164], 16, 3, 15, true, false), 1234); +assertEqual(testSliceToInt([250, 72], 16, 3, 15, false, false), 1234); +assertEqual( + testSliceToInt([250, 72, 223, 189], 32, 7, 29, true, false), + 596983, +); +assertEqual( + testSliceToInt( + [250, 72, 223, 189, 41, 97, 165, 0, 0, 0, 0, 177], + 96, + 14, + 85, + false, + true, + ), + 70821197049655, +); +assertEqual( + testSliceToInt( + [250, 72, 223, 189, 41, 97, 165, 0, 0, 0, 0, 177], + 96, + 14, + 85, + true, + true, + ), + Number(515_906_807_693_217_628_160n), +); // Result.isOk @@ -574,12 +902,6 @@ assertEqual( new ExampleRecordImpl(6, 5, 4), ); -// Test BitArray can only be constructed from Uint8Array, not ArrayBuffer -const bs1 = new BitArray(new Uint8Array(new ArrayBuffer(8))); -assertThrows("Should only construct BitArray from Uint8Array", () => { - const bs = new BitArray(new ArrayBuffer(8)); -}); - // // Summary // diff --git a/test/language/test/language_test.gleam b/test/language/test/language_test.gleam index 62d161c5bcc..f844cd49606 100644 --- a/test/language/test/language_test.gleam +++ b/test/language/test/language_test.gleam @@ -24,6 +24,11 @@ pub fn main() { suite("bit arrays target", bit_array_target_tests()), suite("bit arrays", bit_array_tests()), suite("sized bit arrays", sized_bit_array_tests()), + suite( + "unaligned bit array expressions", + unaligned_bit_array_expression_tests(), + ), + suite("unaligned bit array patterns", unaligned_bit_array_pattern_tests()), suite("list spread", list_spread_tests()), suite("clause guards", clause_guard_tests()), suite("imported custom types", imported_custom_types_test()), @@ -1123,6 +1128,355 @@ fn sized_bit_array_tests() -> List(Test) { ] } +fn unaligned_bit_array_expression_tests() -> List(Test) { + [ + "<<0xFF:6, 0:2>> == <<0xFA>>" + |> example(fn() { assert_equal(True, <<0xFF:6, 0:2>> == <<0xFC>>) }), + "<<0xFF:6, 0:3, 0x75:9>> == <<252, 29, 1:2>>" + |> example(fn() { + assert_equal(True, <<0xFF:6, 0:3, 0x75:9>> == <<252, 29, 1:2>>) + }), + "<<-1:55, 44:11-little, 0x75:9-big>> == <<255, 255, 255, 255, 255, 255, 254, 88, 14, 5:3>>" + |> example(fn() { + assert_equal( + True, + <<-1:55, 44:11-little, 0x75:9-big>> + == <<255, 255, 255, 255, 255, 255, 254, 88, 14, 5:3>>, + ) + }), + "<<0:1, 2:2, 2:3, 1:1>> == <<0b0100101:7>>" + |> example(fn() { + assert_equal(True, <<0:1, 2:2, 2:3, 1:1>> == <<0b0100101:7>>) + }), + "<<-100:6, -10:32-little, -10:32-big, -100:48-big, -100:48-little>> == <<115, 219, 255, 255, 255, 255, 255, 255, 219, 255, 255, 255, 255, 254, 114, 115, 255, 255, 255, 255, 63:6>>" + |> example(fn() { + assert_equal( + True, + <<-100:6, -10:32-little, -10:32-big, -100:48-big, -100:48-little>> + == << + 115, 219, 255, 255, 255, 255, 255, 255, 219, 255, 255, 255, 255, 254, + 114, 115, 255, 255, 255, 255, 63:6, + >>, + ) + }), + "<<2:3, 2.9283123:float-little, -1.375e5:32-float-big>> == <<91, 153, 120, 255, 229, 205, 160, 232, 25, 0, 200, 224, 0:3>>" + |> example(fn() { + assert_equal( + True, + <<2:3, 2.9283123:float-little, -1.375e5:32-float-big>> + == <<91, 153, 120, 255, 229, 205, 160, 232, 25, 0, 200, 224, 0:3>>, + ) + }), + "<<7:6, <<1:3>>:bits, <<1, 2, 3>>:bits, 1:1, <<-1124.789e4:float-little>>:bits>> == <<28, 128, 129, 1, 192, 0, 0, 16, 8, 157, 25, 112, 1:2>>" + |> example(fn() { + assert_equal( + True, + << + 7:6, + <<1:3>>:bits, + <<1, 2, 3>>:bits, + 1:1, + <<-1124.789e4:float-little>>:bits, + >> + == <<28, 128, 129, 1, 192, 0, 0, 16, 8, 157, 25, 112, 1:2>>, + ) + }), + "<<9_444_732_965_739_289_353_650_176:75>> == <<255, 255, 255, 255, 255, 248, 0, 0, 0, 0:size(3)>>" + |> example(fn() { + assert_equal( + True, + <<9_444_732_965_739_289_353_650_176:75>> + == <<255, 255, 255, 255, 255, 248, 0, 0, 0, 0:size(3)>>, + ) + }), + "<<0xFC:6>> == <<<<0xFC>>:bits-6>>" + |> example(fn() { + assert_equal(True, <<0b100101:6>> == <<<<0b10010100>>:bits-6>>) + }), + "<<0xE7>> == <<<<0xEC>>:bits-4, 7:4>>" + |> example(fn() { + assert_equal(True, <<0xE7>> == <<<<0xEC>>:bits-4, 7:4>>) + }), + "<<0b11001:5>> == <<<<0b11011100>>:bits-size(three), 1:size(two)>>" + |> example(fn() { + let three = 3 + let two = 2 + assert_equal( + True, + <<0b11001:5>> == <<<<0b11011100>>:bits-size(three), 1:size(two)>>, + ) + }), + ] +} + +fn unaligned_bit_array_pattern_tests() -> List(Test) { + [ + "let assert <> = <<0xAB, 0b11010101>>" + |> example(fn() { + assert_equal(#(0xA, 0xB, 0b110, 0b1010, 0b1), { + let assert <> = <<0xAB, 0b11010101>> + #(a, b, c, d, e) + }) + }), + "let assert <> = <<0xB6, 0xE3>>" + |> example(fn() { + assert_equal(#(0xB6E, 0x03), { + let assert <> = <<0xB6, 0xE3>> + #(a, b) + }) + }), + "let assert <> = <<0xB6, 0xE3>>" + |> example(fn() { + assert_equal(#(0x0B, 0x6E3), { + let assert <> = <<0xB6, 0xE3>> + #(a, b) + }) + }), + "let assert <> = <<0xB6, 0xE3>>" + |> example(fn() { + assert_equal(#(0xEB6, 0x03), { + let assert <> = <<0xB6, 0xE3>> + #(a, b) + }) + }), + "let assert <> = <<0xB6, 0xE3>>" + |> example(fn() { + assert_equal(#(0x0B, 0x36E), { + let assert <> = <<0xB6, 0xE3>> + #(a, b) + }) + }), + "let assert <> = <<0xB6, 0xE3>>" + |> example(fn() { + assert_equal(#(22, 1763), { + let assert <> = <<0xB6, 0xE3>> + #(a, b) + }) + }), + "let assert <<_:8, a:17>> = <<0xFF, 0xB6, 0xE3, 1:1>>" + |> example(fn() { + assert_equal(93_639, { + let assert <<_:8, a:17>> = <<0xFF, 0xB6, 0xE3, 1:1>> + a + }) + }), + "let assert <> = <<0xA6, 0xE3, 6:3>>" + |> example(fn() { + assert_equal(85_447, { + let assert <> = <<0xA6, 0xE3, 6:3>> + a + }) + }), + "let assert <> = <<0xA6, 0xE3, 6:3>>" + |> example(fn() { + assert_equal(85_447, { + let assert <> = <<0xA6, 0xE3, 6:3>> + a + }) + }), + "let assert <> = <<0b10011010>>" + |> example(fn() { + assert_equal(-51, { + let assert <> = <<0b10011010>> + a + }) + }), + "let assert <<_:5, a:13-big, _:bits>> = <<0xA5, 0x6C, 0xAA>>" + |> example(fn() { + assert_equal(5554, { + let assert <<_:5, a:13-big, _:bits>> = <<0xA5, 0x6C, 0xAA>> + a + }) + }), + "let assert <<_:5, a:13-little-signed, _:bits>> = <<0xA5, 0x6C, 0xAA>>" + |> example(fn() { + assert_equal(-3411, { + let assert <<_:5, a:13-little-signed, _:bits>> = <<0xA5, 0x6C, 0xAA>> + a + }) + }), + "let assert <<_:3, a:8-big-signed, _:bits>> = <<253, 94>>" + |> example(fn() { + assert_equal(-22, { + let assert <<_:3, a:8-big-signed, _:bits>> = <<253, 94>> + a + }) + }), + "let assert <<_:3, a:12-big-unsigned, _:bits>> = <<233, 164>>" + |> example(fn() { + assert_equal(1234, { + let assert <<_:3, a:12-big-unsigned, _:bits>> = <<233, 164>> + a + }) + }), + "let assert <<_:3, a:12-little, _:bits>> = <<250, 72>>" + |> example(fn() { + assert_equal(1234, { + let assert <<_:3, a:12-little, _:bits>> = <<250, 72>> + a + }) + }), + "let assert <<_:7, a:22, _:bits>> = <<250, 72, 223, 189>>" + |> example(fn() { + assert_equal(596_983, { + let assert <<_:7, a:22, _:bits>> = <<250, 72, 223, 189>> + a + }) + }), + "let assert <<_:1, a:23, _:bits>> = <<146, 192, 70, 25, 1:1>>" + |> example(fn() { + assert_equal(1_228_870, { + let assert <<_:1, a:23, _:bits>> = <<146, 192, 70, 25, 1:1>> + a + }) + }), + "let assert <<_:1, a:32>> = <<217, 150, 209, 191, 0:1>>" + |> example(fn() { + assert_equal(3_006_112_638, { + let assert <<_:1, a:32>> = <<217, 150, 209, 191, 0:1>> + a + }) + }), + "let assert <<_:1, a:32-signed>> = <<146, 192, 70, 25, 1:1>>" + |> example(fn() { + assert_equal(629_181_491, { + let assert <<_:1, a:32-signed>> = <<146, 192, 70, 25, 1:1>> + a + }) + }), + "let assert <<_:1, a:32-little-unsigned>> = <<251, 24, 47, 227, 1:1>>" + |> example(fn() { + assert_equal(3_344_904_438, { + let assert <<_:1, a:32-little-unsigned>> = <<251, 24, 47, 227, 1:1>> + a + }) + }), + "let assert <> = <<240, 102, 91, 101, 1:1>>" + |> example(fn() { + assert_equal(5_995_456_240, { + let assert <> = <<240, 102, 91, 101, 1:1>> + a + }) + }), + "let assert <> = <<231, 255, 255, 255, 254, 123>>" + |> example(fn() { + assert_equal(-103_079_215_106, { + let assert <> = <<231, 255, 255, 255, 254, 123>> + a + }) + }), + "let assert <<_:1, a:54-big-unsigned, _:bits>> = <<0, 231, 255, 255, 253, 123, 17>>" + |> example(fn() { + assert_equal(127_543_348_739_464, { + let assert <<_:1, a:54-big-unsigned, _:bits>> = << + 0, 231, 255, 255, 253, 123, 17, + >> + a + }) + }), + "let assert <<_:8, a:54-little-signed, _:bits>> = <<142, 231, 255, 255, 253, 123, 17, 139>>" + |> example(fn() { + assert_equal(-8_425_025_061_257_241, { + let assert <<_:8, a:54-little-signed, _:bits>> = << + 142, 231, 255, 255, 253, 123, 17, 139, + >> + a + }) + }), + "let assert <<_:7, a:55-little-signed, _:bits>> = <<142, 231, 255, 255, 253, 123, 17, 139>>" + |> example(fn() { + assert_equal(-8_293_899_692_933_261, { + let assert <<_:7, a:55-little-signed, _:bits>> = << + 142, 231, 255, 255, 253, 123, 17, 139, + >> + a + }) + }), + "let assert <<_:8, a:40-big-signed, _:8>> = <<142, 231, 255, 255, 253, 123, 17>>" + |> example(fn() { + assert_equal(-103_079_215_749, { + let assert <<_:8, a:40-big-signed, _:8>> = << + 142, 231, 255, 255, 253, 123, 17, + >> + a + }) + }), + "let assert <<_:14, a:71-little-signed, _:bits>> = <<250, 72, 223, 189, 41, 97, 165, 0, 0, 0, 0, 177>>" + |> example(fn() { + assert_equal(70_821_197_049_655, { + let assert <<_:14, a:71-little-signed, _:bits>> = << + 250, 72, 223, 189, 41, 97, 165, 0, 0, 0, 0, 177, + >> + a + }) + }), + "let assert <<_:14, a:71-big-signed, _:bits>> = <<250, 72, 223, 189, 41, 97, 165, 0, 0, 0, 0, 177>>" + |> example(fn() { + assert_equal(515_906_807_693_217_628_160, { + let assert <<_:14, a:71-big-signed, _:bits>> = << + 250, 72, 223, 189, 41, 97, 165, 0, 0, 0, 0, 177, + >> + a + }) + }), + "let assert <<_:11, a:float-32, _:bits>> = <<112, 152, 127, 244, 0, 7, 0:2>>" + |> example(fn() { + assert_equal(-511.25, { + let assert <<_:11, a:float-32, _:bits>> = << + 112, 152, 127, 244, 0, 7, 0:2, + >> + a + }) + }), + "let assert <<_:7, a:float-little, _:bits>> = <<8, 0, 0, 0, 1, 129, 39, 103, 129, 127:7>>" + |> example(fn() { + assert_equal(-5011.75, { + let assert <<_:7, a:float-little, _:bits>> = << + 8, 0, 0, 0, 1, 129, 39, 103, 129, 127:7, + >> + a + }) + }), + "let assert <> = <<0b11001011, 0b01:2>>" + |> example(fn() { + assert_equal(#(<<0b1100101:7>>, <<0b101:3>>), { + let assert <> = <<0b11001011, 0b01:2>> + #(a, b) + }) + }), + "let assert <> = <<0x47, 0x9A, 0x25, 0x0C, 0xDA, 0xF1, 0xEE, 0x31>>" + |> example(fn() { + assert_equal( + #(<<71, 154>>, <<37, 1:5>>, <<155, 2:3>>, <<0xF1, 0xEE, 0x31>>), + { + let assert <> = << + 0x47, 0x9A, 0x25, 0x0C, 0xDA, 0xF1, 0xEE, 0x31, + >> + #(a, b, c, d) + }, + ) + }), + "let assert <> = <<0b110:3, 0x12, 0xAB, 0x95, 0xFE>>" + |> example(fn() { + assert_equal(#(<<0b110:3>>, <<0x12, 0xAB>>, <<0x95, 0xFE>>), { + let assert <> = << + 0b110:3, 0x12, 0xAB, 0x95, 0xFE, + >> + #(a, b, c) + }) + }), + "let assert <<0x12, _:5, _:bytes>> = <<0x12, 0xFF:6, 0x95, 0xFE>>" + |> example(fn() { + assert_equal(False, { + case <<0x12, 0xAB, 0x95, 0xFE>> { + <<0x34, _:5, _:bytes>> -> True + _ -> False + } + }) + }), + ] +} + fn list_spread_tests() -> List(Test) { [ "[1, ..[]]" @@ -1469,6 +1823,24 @@ fn bit_array_match_tests() { a }) }), + "let <> = <<0x00, 0xaa, 255, 255, 255, 255, 255>>" + |> example(fn() { + assert_equal(0xaaffffffffff, { + let assert <> = << + 0x00, 0xaa, 255, 255, 255, 255, 255, + >> + a + }) + }), + "let <> = <<255, 255, 255, 255, 255, 0xaa, 0x00>>" + |> example(fn() { + assert_equal(0xaaffffffffff, { + let assert <> = << + 255, 255, 255, 255, 255, 0xaa, 0x00, + >> + a + }) + }), "let <> = <<63,240,0,0,0,0,0,0,1>>" |> example(fn() { assert_equal(#(1.0, 1), { @@ -1556,6 +1928,22 @@ fn bit_array_match_tests() { |> example(fn() { assert_equal(True, <<71, 108, 101, 97, 109>> == <<"Gleam":utf8>>) }), + "<> == <<231, 255, 255, 255, 254, 123>>" + |> example(fn() { + assert_equal(-103_079_215_106, { + let assert <> = <<231, 255, 255, 255, 254, 123>> + i + }) + }), + "<<_, i:40-signed, _:bits>> == <<142, 231, 255, 255, 253, 123, 17>>" + |> example(fn() { + assert_equal(-103_079_215_749, { + let assert <<_, i:40-signed, _:bits>> = << + 142, 231, 255, 255, 253, 123, 17, + >> + i + }) + }), ] }