diff --git a/README.md b/README.md index 41226b2..e54c0ca 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Some tasks below are marked easy if you want to get started. This is a list of all outstanding `todo` expressions (Gleam todo expressions are ) in the codebase, as of the last time that I updated this list. -- imports with attributes are not implemented yet +- imports with attributes are not handled yet - type imports are not implemented yet - Unlabelled fields in custom types are not generated yet - Given that labelled and unlabelled fields can be mixed on one class, I have diff --git a/src/compiler/internal/generator/expressions.gleam b/src/compiler/internal/generator/expressions.gleam index 62ffdef..29fa188 100644 --- a/src/compiler/internal/generator/expressions.gleam +++ b/src/compiler/internal/generator/expressions.gleam @@ -107,6 +107,8 @@ pub fn generate_expression(expression: python.Expression) -> StringBuilder { python.BinaryOperator(name, left, right) -> generate_binop(name, left, right) + + python.BitString(segments) -> generate_bitstring(segments) } } @@ -161,3 +163,46 @@ fn generate_binop( |> string_builder.append(op_string) |> string_builder.append_builder(generate_expression(right)) } + +fn generate_bitstring(segments: List(python.BitStringSegment)) -> StringBuilder { + string_builder.from_string("gleam_bitstring_segments_to_bytes(") + |> string_builder.append_builder(internal.generate_plural( + segments, + generate_bitstring_segment, + ", ", + )) + |> string_builder.append(")") +} + +fn generate_bitstring_segment(segment: python.BitStringSegment) -> StringBuilder { + generate_expression(segment.value) + |> string_builder.prepend("(") + |> string_builder.append(", [") + |> string_builder.append_builder(internal.generate_plural( + segment.options, + generate_bitstring_segment_option, + ", ", + )) + |> string_builder.append("])") +} + +fn generate_bitstring_segment_option( + option: python.BitStringSegmentOption, +) -> StringBuilder { + case option { + python.SizeValueOption(expression) -> + generate_expression(expression) + |> string_builder.prepend("\"SizeValue\", ") + python.UnitOption(integer) -> + integer + |> int.to_string + |> string_builder.from_string + |> string_builder.prepend("\"Unit\", ") + python.FloatOption -> string_builder.from_string("\"Float\", None") + python.BigOption -> string_builder.from_string("\"Big\", None") + python.LittleOption -> string_builder.from_string("\"Little\", None") + python.NativeOption -> string_builder.from_string("\"Native\", None") + } + |> string_builder.prepend("(") + |> string_builder.append(")") +} diff --git a/src/compiler/internal/transformer.gleam b/src/compiler/internal/transformer.gleam index 71fd636..5918b73 100644 --- a/src/compiler/internal/transformer.gleam +++ b/src/compiler/internal/transformer.gleam @@ -98,7 +98,7 @@ pub fn merge_state_prepend( prev: TransformState(ReversedList(a)), current: ExpressionReturn, map_next: fn(python.Expression) -> a, -) -> TransformState(List(a)) { +) -> TransformState(ReversedList(a)) { merge_state( prev, current, @@ -106,6 +106,13 @@ pub fn merge_state_prepend( ) } +pub fn map_state_prepend( + prev: TransformState(ReversedList(a)), + next: a, +) -> TransformState(ReversedList(a)) { + TransformState(prev.context, prev.statements, prev.item |> list.prepend(next)) +} + pub fn reverse_state_to_return( state: TransformState(ReversedList(a)), mapper: fn(List(a)) -> python.Expression, diff --git a/src/compiler/internal/transformer/statements.gleam b/src/compiler/internal/transformer/statements.gleam index 416c28c..8a43f9c 100644 --- a/src/compiler/internal/transformer/statements.gleam +++ b/src/compiler/internal/transformer/statements.gleam @@ -179,9 +179,13 @@ fn transform_expression( glance.RecordUpdate(record:, fields:, ..) -> transform_record_update(context, record, fields) - glance.BitString(_) as expr -> { - pprint.debug(expr) - todo as "BitString expressions not supported yet" + glance.BitString(segments) -> { + segments + |> list.fold( + internal.TransformState(context, [], []), + fold_bitstring_segment, + ) + |> internal.reverse_state_to_return(python.BitString) } } } @@ -488,7 +492,7 @@ fn transform_record_update( let record_result = transform_expression(context, record) fields |> list.fold( - internal.TransformState(record_result.context, [], []), + internal.TransformState(record_result.context, record_result.statements, []), fn(state, tuple) { internal.merge_state_prepend( state, @@ -502,3 +506,76 @@ fn transform_record_update( fields: _, )) } + +fn fold_bitstring_segment( + state: internal.TransformState(internal.ReversedList(python.BitStringSegment)), + segment: #( + glance.Expression, + List(glance.BitStringSegmentOption(glance.Expression)), + ), +) -> internal.TransformState(internal.ReversedList(python.BitStringSegment)) { + let #(expression, options) = segment + let expression_result = transform_expression(state.context, expression) + let options_result = + options + |> list.fold( + internal.TransformState( + expression_result.context, + expression_result.statements, + [], + ), + fold_bitsting_segment_option, + ) + + internal.TransformState( + options_result.context, + options_result.statements, + list.prepend( + state.item, + python.BitStringSegment( + expression_result.expression, + options_result.item |> list.reverse, + ), + ), + ) +} + +fn fold_bitsting_segment_option( + state: internal.TransformState( + internal.ReversedList(python.BitStringSegmentOption), + ), + option: glance.BitStringSegmentOption(glance.Expression), +) -> internal.TransformState( + internal.ReversedList(python.BitStringSegmentOption), +) { + case option { + glance.FloatOption -> internal.map_state_prepend(state, python.FloatOption) + glance.LittleOption -> + internal.map_state_prepend(state, python.LittleOption) + glance.BigOption -> internal.map_state_prepend(state, python.BigOption) + glance.NativeOption -> + internal.map_state_prepend(state, python.NativeOption) + glance.UnitOption(size) -> + internal.map_state_prepend(state, python.UnitOption(size)) + glance.SizeOption(size) -> + internal.map_state_prepend( + state, + python.SizeValueOption(python.Number(size |> int.to_string)), + ) + glance.SizeValueOption(expression) -> { + let expression_result = transform_expression(state.context, expression) + internal.merge_state_prepend( + state, + expression_result, + python.SizeValueOption, + ) + } + glance.SignedOption | glance.UnsignedOption -> { + panic as "Signed and unsigned are not valid when constructing bitstrings" + } + _ -> { + pprint.debug(option) + todo as "Some bitstring segment options not supported yet" + } + } +} diff --git a/src/compiler/python.gleam b/src/compiler/python.gleam index a2af3e8..178fb6c 100644 --- a/src/compiler/python.gleam +++ b/src/compiler/python.gleam @@ -48,6 +48,20 @@ pub type Expression { Call(function: Expression, arguments: List(Field(Expression))) RecordUpdate(record: Expression, fields: List(Field(Expression))) BinaryOperator(name: BinaryOperator, left: Expression, right: Expression) + BitString(List(BitStringSegment)) +} + +pub type BitStringSegment { + BitStringSegment(value: Expression, options: List(BitStringSegmentOption)) +} + +pub type BitStringSegmentOption { + SizeValueOption(Expression) + UnitOption(Int) + FloatOption + LittleOption + BigOption + NativeOption } pub type Statement { diff --git a/src/python_prelude.gleam b/src/python_prelude.gleam index fe8e9fd..e772642 100644 --- a/src/python_prelude.gleam +++ b/src/python_prelude.gleam @@ -1,12 +1,14 @@ pub const gleam_builtins = " import dataclasses +import sys import typing +import struct class GleamPanic(BaseException): pass -GleamListElem = typing.TypeVar(\"GleamListElem\") +GleamListElem = typing.TypeVar('GleamListElem') class GleamList(typing.Generic[GleamListElem]): @@ -18,11 +20,11 @@ class GleamList(typing.Generic[GleamListElem]): strs.append(head.value) head = head.tail - return \"GleamList([\" + \", \".join(strs) + \"])\" + return 'GleamList([' + ', '.join(strs) + '])' class NonEmptyGleamList(GleamList[GleamListElem]): - __slots__ = [\"value\", \"tail\"] + __slots__ = ['value', 'tail'] is_empty = False def __init__(self, value: GleamListElem, tail: GleamList[GleamListElem]): @@ -41,6 +43,83 @@ def to_gleam_list(elements: list[GleamListElem], tail: GleamList = EmptyGleamLis for element in reversed(elements): head = NonEmptyGleamList(element, head) return head + +def gleam_bitstring_segments_to_bytes(*segments): + result = bytearray() + for segment in segments: + result.extend(gleam_bitstring_segment_to_bytes(segment)) + return bytes(result) + + +def gleam_bitstring_segment_to_bytes(segment) -> bytes: + value, options = segment + + size = None + unit = None + type = None + endianness = 'big' + for option in options: + match option: + case ('SizeValue', size): + size = size + case('Unit', unit): + unit = unit + case('Little', None): + endianness = 'little' + case('Big', None): + endianness = 'big' + case('Native', None): + endianness = sys.byteorder + case('Float', None): + type = 'float' + case _: + raise Exception(f'Unexpected bitstring option {option}') + + # Defaults from https://www.erlang.org/doc/system/bit_syntax.html + if type == None: + type = 'int' + + if size == None: + match type: + case 'int': + size = 8 + case 'float': + size = 64 + + if unit == None: + match type: + case 'int': + unit = 1 + case 'float': + unit = 1 + + bitsize = unit * size + if bitsize % 8: + raise Exception(f'Python bitstrings must be byte aligned, but got {bitsize}') + + bytesize = bitsize // 8 + + match type: + case 'int': + return value.to_bytes(bitsize // 8, endianness, signed=value < 0) + case 'float': + match endianness: + case 'big': + order = '>' + case 'little': + order = '<' + case 'native': + onder = '=' + match bitsize: + case 32: + fmt = 'f' + case 64: + fmt = 'd' + case _: + raise Exception('bitstring floats must be 32 or 64 bits') + return struct.pack(f'{order}{fmt}', value) + + raise Exception('Unexpected bitstring encountered') " pub const prelude = "from gleam_builtins import *\n\n" diff --git a/test/bitstring_test.gleam b/test/bitstring_test.gleam new file mode 100644 index 0000000..1729aca --- /dev/null +++ b/test/bitstring_test.gleam @@ -0,0 +1,184 @@ +import compiler +import gleeunit/should + +pub fn single_byte_case_test() { + "pub fn main() { + <<16>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((16, []))", + ) +} + +pub fn multiple_bytes_case_test() { + "pub fn main() { + <<16, 42, 255>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((16, []), (42, []), (255, []))", + ) +} + +pub fn two_byte_integers_test() { + "pub fn main() { + <<62_000:16, 63_000:16>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((62_000, [(\"SizeValue\", 16)]), (63_000, [(\"SizeValue\", 16)]))", + ) +} + +pub fn size_expression_test() { + "pub fn main() { + let x = 16 + <<62_000:size(x)>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + x = 16 + return gleam_bitstring_segments_to_bytes((62_000, [(\"SizeValue\", x)]))", + ) +} + +pub fn little_endian_test() { + "pub fn main() { + <<4_666_000:32-little>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((4_666_000, [(\"SizeValue\", 32), (\"Little\", None)]))", + ) +} + +pub fn big_endian_test() { + "pub fn main() { + <<4_666_000:32-big>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((4_666_000, [(\"SizeValue\", 32), (\"Big\", None)]))", + ) +} + +pub fn native_endian_test() { + "pub fn main() { + <<4_666_000:32-native>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((4_666_000, [(\"SizeValue\", 32), (\"Native\", None)]))", + ) +} + +pub fn size_unit_test() { + "pub fn main() { + <<64_003:2-unit(8)>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((64_003, [(\"SizeValue\", 2), (\"Unit\", 8)]))", + ) +} + +pub fn float_default_double_test() { + "pub fn main() { + <<64.888889:float>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((64.888889, [(\"Float\", None)]))", + ) +} + +pub fn float_explicit_double_test() { + "pub fn main() { + <<64.888889:64-float>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((64.888889, [(\"SizeValue\", 64), (\"Float\", None)]))", + ) +} + +pub fn float_explicit_single_test() { + "pub fn main() { + <<64.888889:32-float>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((64.888889, [(\"SizeValue\", 32), (\"Float\", None)]))", + ) +} + +pub fn float_single_little_test() { + "pub fn main() { + <<64.888889:32-float>> + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + return gleam_bitstring_segments_to_bytes((64.888889, [(\"SizeValue\", 32), (\"Float\", None)]))", + ) +}