From 040d41d93fe4003760b1e350269983aa753b63fe Mon Sep 17 00:00:00 2001 From: Dusty Phillips Date: Sun, 25 Aug 2024 09:46:41 -0300 Subject: [PATCH] Support list patterns --- README.md | 3 +- .../internal/generator/statements.gleam | 19 +++ .../internal/transformer/patterns.gleam | 6 +- src/compiler/python.gleam | 1 + src/python_prelude.gleam | 32 ++-- test/case_test.gleam | 149 ++++++++++++++++++ 6 files changed, 188 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d572ff3..8bd489f 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ are ) in the codebase, as of the last time that I updated this list. - debatable whether to make them a `def` right on the class or have the def be defined somewhere and just attach it like other fields - Fields that are "HoleType" are not supported and I don't even know what that means - IN case statements, the following patterns are not supported: - - List patterns - Concatenate patterns - Bitstring patterns (bytes) - Destructuring in assignments is not supported yet @@ -85,7 +84,7 @@ are ) in the codebase, as of the last time that I updated this list. - other structures will maybe need a match statement? - Use statements are not supported yet -### Some other the things I know are missing +### Some other things I know are missing - empty tuples are probably broken - (EASY) I used parens instead of a `,` like a total python NOOB diff --git a/src/compiler/internal/generator/statements.gleam b/src/compiler/internal/generator/statements.gleam index 740ba72..e8768b8 100644 --- a/src/compiler/internal/generator/statements.gleam +++ b/src/compiler/internal/generator/statements.gleam @@ -94,6 +94,7 @@ fn generate_pattern(pattern: python.Pattern) -> StringBuilder { |> string_builder.join(", ") |> string_builder.prepend("(") |> string_builder.append(")") + python.PatternList(elements, rest) -> generate_pattern_list(elements, rest) python.PatternAlternate(patterns) -> patterns |> list.map(generate_pattern) @@ -123,3 +124,21 @@ fn generate_pattern_constructor_field( python.UnlabelledField(pattern) -> generate_pattern(pattern) } } + +/// Lists are weird. Gleam syntax is like [a, b, c, ..rest] +/// But the pattern in python has to match a linked list. +/// The pattern is essentially GleamList(a, GleamList(b, GleamList(c, rest))) +/// potential optimization: make tail recursive by carrying the number of +/// closing parenns forward +fn generate_pattern_list(elements, rest) -> StringBuilder { + case elements, rest { + [], option.None -> string_builder.from_string("None") + [], option.Some(pattern) -> generate_pattern(pattern) + [head, ..others], rest -> + string_builder.from_string("GleamList(") + |> string_builder.append_builder(generate_pattern(head)) + |> string_builder.append(", ") + |> string_builder.append_builder(generate_pattern_list(others, rest)) + |> string_builder.append(")") + } +} diff --git a/src/compiler/internal/transformer/patterns.gleam b/src/compiler/internal/transformer/patterns.gleam index f2789cc..9a79030 100644 --- a/src/compiler/internal/transformer/patterns.gleam +++ b/src/compiler/internal/transformer/patterns.gleam @@ -42,7 +42,11 @@ fn transform_pattern(pattern: glance.Pattern) -> python.Pattern { glance.PatternDiscard(str) -> python.PatternVariable("_" <> str) glance.PatternTuple(patterns) -> python.PatternTuple(list.map(patterns, transform_pattern)) - glance.PatternList(_, _) -> todo as "list patterns are not supported yet" + glance.PatternList(elems, rest) -> + python.PatternList( + list.map(elems, transform_pattern), + option.map(rest, transform_pattern), + ) glance.PatternAssignment(pattern, name) -> python.PatternAssignment(transform_pattern(pattern), name) glance.PatternConcatenate(_, _) -> diff --git a/src/compiler/python.gleam b/src/compiler/python.gleam index 6772d6a..582ee6b 100644 --- a/src/compiler/python.gleam +++ b/src/compiler/python.gleam @@ -118,6 +118,7 @@ pub type Pattern { PatternVariable(value: String) PatternAssignment(pattern: Pattern, name: String) PatternTuple(value: List(Pattern)) + PatternList(elems: List(Pattern), rest: option.Option(Pattern)) PatternAlternate(patterns: List(Pattern)) PatternConstructor( module: option.Option(String), diff --git a/src/python_prelude.gleam b/src/python_prelude.gleam index 104c99a..a8022ee 100644 --- a/src/python_prelude.gleam +++ b/src/python_prelude.gleam @@ -1,4 +1,5 @@ pub const gleam_builtins = " +from __future__ import annotations import dataclasses import sys import typing @@ -12,36 +13,29 @@ GleamListElem = typing.TypeVar('GleamListElem') class GleamList(typing.Generic[GleamListElem]): + __slots__ = [\"value\", \"tail\"] + __match_args__ = (\"value\", \"tail\") + + def __init__(self, value: GleamListElem, tail: GleamList[GleamListElem] | None): + self.value = value + self.tail = tail + def __str__(self): strs = [] head = self - while not head.is_empty: - strs.append(head.value) + while head is not None: + strs.append(str(head.value)) head = head.tail - return 'GleamList([' + ', '.join(strs) + '])' - - -class NonEmptyGleamList(GleamList[GleamListElem]): - __slots__ = ['value', 'tail'] - is_empty = False - - def __init__(self, value: GleamListElem, tail: GleamList[GleamListElem]): - self.value = value - self.tail = tail - - -class EmptyGleamList(GleamList): - __slots__ = [] - is_empty = True + return \"GleamList([\" + \", \".join(strs) + \"])\" -def to_gleam_list(elements: list[GleamListElem], tail: GleamList = EmptyGleamList()): +def to_gleam_list(elements: list[GleamListElem], tail: GleamList | None=None): head = tail for element in reversed(elements): - head = NonEmptyGleamList(element, head) + head = GleamList(element, head) return head def gleam_bitstring_segments_to_bytes(*segments): diff --git a/test/case_test.gleam b/test/case_test.gleam index 1958d3e..7a586bb 100644 --- a/test/case_test.gleam +++ b/test/case_test.gleam @@ -216,3 +216,152 @@ def main(): return _fn_case_0(1)", ) } + +pub fn case_empty_list_test() { + "pub fn main() { + case [] { + [] -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case None: + return 1 + return _fn_case_0(to_gleam_list([]))", + ) +} + +pub fn case_single_element_list_test() { + "pub fn main() { + case [1] { + [1] -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case GleamList(1, None): + return 1 + return _fn_case_0(to_gleam_list([1]))", + ) +} + +pub fn case_multi_element_list_test() { + "pub fn main() { + case [1, 2, 3] { + [1, 2, 3] -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case GleamList(1, GleamList(2, GleamList(3, None))): + return 1 + return _fn_case_0(to_gleam_list([1, 2, 3]))", + ) +} + +// The gleam formatter doesn't permit this scenario, but it is encountered +// during recursion +pub fn case_empty_rest_case_test() { + "pub fn main() { + case [1, 2, 3] { + rest -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case rest: + return 1 + return _fn_case_0(to_gleam_list([1, 2, 3]))", + ) +} + +pub fn single_element_with_rest_case_test() { + "pub fn main() { + case [1, 2, 3] { + [1, ..rest] -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case GleamList(1, rest): + return 1 + return _fn_case_0(to_gleam_list([1, 2, 3]))", + ) +} + +pub fn multi_element_with_rest_case_test() { + "pub fn main() { + case [1, 2, 3] { + [1, 2, ..rest] -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case GleamList(1, GleamList(2, rest)): + return 1 + return _fn_case_0(to_gleam_list([1, 2, 3]))", + ) +} + +pub fn unnamed_rest_test() { + "pub fn main() { + case [1, 2, 3] { + [1, 2, ..] -> 1 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case GleamList(1, GleamList(2, _)): + return 1 + return _fn_case_0(to_gleam_list([1, 2, 3]))", + ) +}