Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support more bit array options for ints and floats on JavaScript #3293

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

richard-viney
Copy link
Contributor

This PR adds support for the following options for floats and ints in bit array patterns & expressions on the JavaScript target:

  • Explicit size on floats: 64-bit and 32-bit. 16-bit floats aren't included, though they are supported by Erlang.
  • Explicit endianness on floats and ints via big and little options. native endianness hasn't been added, but potentially could be. big remains the default.
  • Explicit signedness on ints via signed and unsigned.

Implementation notes:

  1. There was some pre-existing code duplication in expression.rs for bit arrays in constant expressions. This PR hasn't aimed to fix this, and there's now a bit more code duplication of a similar nature in expression.rs as a result.
  2. There was one place where it was incorrectly assumed that the JavaScript platform is little endian (in the use of Float64Array). Most clients these days are indeed little endian, so it wasn't much of a problem in practice, but I've changed the code to use DataView instead.
  3. There are non-backwards compatible changes to function signatures in the JavaScript prelude, but affected functions were all tagged with // @internal so I assumed that it's ok to change these.

@richard-viney richard-viney changed the title Int/float size, endianness, and signed Support more bit array options for ints and floats on JavaScript Jun 19, 2024
@richard-viney richard-viney force-pushed the js_float_int_bit_array_options branch from b71d160 to 773c3d0 Compare June 19, 2024 04:16
Copy link
Member

@lpil lpil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Thank you! I've left some notes inline.

let default_size = if segment.type_ == crate::type_::int() {
8usize
} else {
64usize
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this only be for floats?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ints have an 8-bit size by default?

compiler-core/src/javascript/expression.rs Show resolved Hide resolved
Comment on lines 17 to 18
IntFromSlice(usize, usize, bool, bool),
FloatFromSlice(usize, usize, bool),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert these to struct variants IntFromSlice { a: ..., b: ...} please as I don't understand what the 4 unnamed fields are here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

fn push_float_at(&mut self, i: usize) {
self.path.push(Index::FloatAt(i));
fn push_float_from_slice(&mut self, i: usize, end: usize, is_big_endian: bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use an enum for endianness

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I wasn't totally sure where to put it, so it's in its own file. Please let me know if it belongs better somewhere else.

self.pop();
offset.increment(1);
Ok(())
if segment.type_ == crate::type_::int()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as above with doing it in two passes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as per previous comment

@@ -50,6 +50,24 @@ fn float() {
r#"
fn go() {
<<1.1:float>>
<<1.1:float-big>>
<<1.1:float-little>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each test should only test one thing, so make a new test for each of these new examples please 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

const msg = `Bit arrays must be byte aligned on JavaScript, got size of ${size} bits`;
throw new globalThis.Error(msg);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put back the byte alignment check please 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I thought it was no longer needed since the check added in 6d168ef, but reading the commit more carefully I was 100% wrong about that.

return view.getFloat64(start, !isBigEndian)
} else if (byteSize === 4) {
return view.getFloat32(start, !isBigEndian)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function returns undefined when byteSize is not 8 or 4. It must always return a valid value.

Rather than have an end we could have a second bool for whether it is 32 or 64 bits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering adding 16-bit floats to JS as well in a separate PR, hence the usage of start & end here to match byteArrayToInt and also allow for 16-bit floats.

Bit array pattern matching on JS doesn't yet allow dynamic sizing, i.e. the size must be a compile-time const, and existing checks ensure that it is 16, 32, or 64.

I've added a check to the JS side so there's a compile error if a float size of 16 is given, as this isn't currently supported.

Given those two points, byteArrayToFloat can't ever be called with a byte size other than 4 or 8. For safety, I've added a check that throws an error to avoid undefined being silently returned, but it shouldn't be possible to ever get there.

Thoughts?

export function float64Bits(float) {
return new Uint8Array(Float64Array.from([float]).buffer).reverse();
export function sizedFloat(float, size, isBigEndian) {
if (size !== 32 && size !== 64) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a bool as that forces it to be one or the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's an option because the size can be specified dynamically:

  let a = int.random(1000)
  let b = <<1.0:float-size(a)>>

Both Erlang and JS throw an error if the size at runtime isn't supported for floats.

@richard-viney richard-viney force-pushed the js_float_int_bit_array_options branch from 773c3d0 to 513f386 Compare June 21, 2024 01:10
Copy link
Contributor Author

@richard-viney richard-viney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, all comments have been addressed or responded to, ready for another look 👍

compiler-core/src/javascript/expression.rs Show resolved Hide resolved
let default_size = if segment.type_ == crate::type_::int() {
8usize
} else {
64usize
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ints have an 8-bit size by default?

Comment on lines 17 to 18
IntFromSlice(usize, usize, bool, bool),
FloatFromSlice(usize, usize, bool),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

fn push_float_at(&mut self, i: usize) {
self.path.push(Index::FloatAt(i));
fn push_float_from_slice(&mut self, i: usize, end: usize, is_big_endian: bool) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I wasn't totally sure where to put it, so it's in its own file. Please let me know if it belongs better somewhere else.

const msg = `Bit arrays must be byte aligned on JavaScript, got size of ${size} bits`;
throw new globalThis.Error(msg);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I thought it was no longer needed since the check added in 6d168ef, but reading the commit more carefully I was 100% wrong about that.

return view.getFloat64(start, !isBigEndian)
} else if (byteSize === 4) {
return view.getFloat32(start, !isBigEndian)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering adding 16-bit floats to JS as well in a separate PR, hence the usage of start & end here to match byteArrayToInt and also allow for 16-bit floats.

Bit array pattern matching on JS doesn't yet allow dynamic sizing, i.e. the size must be a compile-time const, and existing checks ensure that it is 16, 32, or 64.

I've added a check to the JS side so there's a compile error if a float size of 16 is given, as this isn't currently supported.

Given those two points, byteArrayToFloat can't ever be called with a byte size other than 4 or 8. For safety, I've added a check that throws an error to avoid undefined being silently returned, but it shouldn't be possible to ever get there.

Thoughts?

export function float64Bits(float) {
return new Uint8Array(Float64Array.from([float]).buffer).reverse();
export function sizedFloat(float, size, isBigEndian) {
if (size !== 32 && size !== 64) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's an option because the size can be specified dynamically:

  let a = int.random(1000)
  let b = <<1.0:float-size(a)>>

Both Erlang and JS throw an error if the size at runtime isn't supported for floats.

self.pop();
offset.increment(1);
Ok(())
if segment.type_ == crate::type_::int()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as per previous comment

@@ -50,6 +50,24 @@ fn float() {
r#"
fn go() {
<<1.1:float>>
<<1.1:float-big>>
<<1.1:float-little>>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@richard-viney richard-viney force-pushed the js_float_int_bit_array_options branch 3 times, most recently from 544ad7b to 751dee0 Compare June 28, 2024 22:56
@richard-viney richard-viney force-pushed the js_float_int_bit_array_options branch from 751dee0 to 55d0cd5 Compare July 1, 2024 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants