diff --git a/README.md b/README.md index 20aad29..bf933ab 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,10 @@ PRs are welcome. ### Some of the things I know are missing - no destructuring/pattern matching in let + - some constructs can map straight to python destructuring + - others may need a custom case - no let assert -- no case expressions +- empty tuples are probably broken - label aliases aren't supported yet (ie `fn foo(bar bas: Str)`) - const definitions aren't supported yet (module.constants) - type aliases aren't supported yet (module.type_aliases) diff --git a/src/compiler/generator.gleam b/src/compiler/generator.gleam index bc3c04b..06462da 100644 --- a/src/compiler/generator.gleam +++ b/src/compiler/generator.gleam @@ -4,6 +4,7 @@ import gleam/int import gleam/list import gleam/option import gleam/string_builder.{type StringBuilder} +import pprint import python_prelude fn generate_import(import_: python.Import) -> StringBuilder { @@ -191,17 +192,64 @@ fn generate_statement(statement: python.Statement) -> StringBuilder { |> string_builder.append(" = ") |> string_builder.append_builder(generate_expression(value)) } + python.Match(cases) -> + string_builder.new() + |> string_builder.append("match _case_subject:\n") + |> string_builder.append_builder( + generate_cases(cases) |> internal.indent(4), + ) + // TODO: Deal with cases python.FunctionDef(function) -> generate_function(function) } } +fn generate_cases(cases: List(python.MatchCase)) -> StringBuilder { + case cases { + [] -> string_builder.from_string("pass") + cases -> internal.generate_plural(cases, generate_case, "\n") + } +} + +fn generate_case(case_: python.MatchCase) -> StringBuilder { + string_builder.from_string("case ") + |> string_builder.append_builder(generate_pattern(case_.pattern)) + |> string_builder.append(":\n") + |> string_builder.append_builder( + generate_block(case_.body) |> internal.indent(4), + ) +} + +fn generate_pattern(pattern: python.Pattern) -> StringBuilder { + case pattern { + python.PatternWildcard -> string_builder.from_string("_") + python.PatternInt(str) + | python.PatternFloat(str) + | python.PatternVariable(str) -> string_builder.from_string(str) + python.PatternString(str) -> string_builder.from_strings(["\"", str, "\""]) + python.PatternAssignment(pattern, name) -> + generate_pattern(pattern) + |> string_builder.append(" as ") + |> string_builder.append(name) + python.PatternTuple(patterns) -> + patterns + |> list.map(generate_pattern) + |> string_builder.join(", ") + |> string_builder.prepend("(") + |> string_builder.append(")") + python.PatternAlternate(patterns) -> + patterns + |> list.map(generate_pattern) + |> string_builder.join(" | ") + } +} + fn generate_parameter(param: python.FunctionParameter) -> StringBuilder { case param { python.NameParam(name) -> string_builder.from_string(name) } } -fn generate_function_body(statements: List(python.Statement)) -> StringBuilder { +fn generate_block(statements: List(python.Statement)) -> StringBuilder { case statements { [] -> string_builder.from_string("pass") multiple_lines -> @@ -222,7 +270,7 @@ fn generate_function(function: python.Function) -> StringBuilder { )) |> string_builder.append("):\n") |> string_builder.append_builder( - generate_function_body(function.body) |> internal.indent(4), + generate_block(function.body) |> internal.indent(4), ) } diff --git a/src/compiler/internal/transformer.gleam b/src/compiler/internal/transformer.gleam index 028dfa2..7c4eef8 100644 --- a/src/compiler/internal/transformer.gleam +++ b/src/compiler/internal/transformer.gleam @@ -11,7 +11,11 @@ pub type TransformError { } pub type TransformerContext { - TransformerContext(next_function_id: Int, next_block_id: Int) + TransformerContext( + next_function_id: Int, + next_block_id: Int, + next_case_id: Int, + ) } pub type ExpressionReturn { diff --git a/src/compiler/internal/transformer/patterns.gleam b/src/compiler/internal/transformer/patterns.gleam new file mode 100644 index 0000000..b13dd27 --- /dev/null +++ b/src/compiler/internal/transformer/patterns.gleam @@ -0,0 +1,55 @@ +import compiler/python +import glance +import gleam/list +import pprint + +// alternative patterns are sent to us a a list of list of patters. +// the outer list represents alternatives, so 1 | 2 -> becomes [[1], [2]] +// inner loop represents groupings (see `transform_grouped_pattrns`) +// so 1, 2 | 3, 5 becomes [1, 2], [3, 5] +pub fn transform_alternative_patterns( + patterns: List(List(glance.Pattern)), +) -> python.Pattern { + case patterns { + [] -> panic as "missing pattern" + [one_alternative] -> transform_grouped_patterns(one_alternative) + multiple_alternatives -> + python.PatternAlternate(list.map( + multiple_alternatives, + transform_grouped_patterns, + )) + } +} + +// gleam distinguishes between groups of patterns (e.g: case 1, 2 {x, y -> ...}) +// and glance sends those to us as a list of patterns. The python pattern +// for a group of patterns will always be a single tuple pattern. +fn transform_grouped_patterns(patterns: List(glance.Pattern)) -> python.Pattern { + case patterns { + [] -> panic as "missing pattern" + [one_item] -> transform_pattern(one_item) + multiple_items -> transform_pattern(glance.PatternTuple(multiple_items)) + } +} + +fn transform_pattern(pattern: glance.Pattern) -> python.Pattern { + case pattern { + glance.PatternInt(str) -> python.PatternInt(str) + glance.PatternFloat(str) -> python.PatternFloat(str) + glance.PatternString(str) -> python.PatternString(str) + glance.PatternVariable(str) -> python.PatternVariable(str) + glance.PatternDiscard("") -> python.PatternWildcard + 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.PatternAssignment(pattern, name) -> + python.PatternAssignment(transform_pattern(pattern), name) + glance.PatternConcatenate(_, _) -> + todo as "concatenate patterns are not supported yet" + glance.PatternBitString(..) -> + todo as "bitstring patterns are not supported yet" + glance.PatternConstructor(..) -> + todo as "record constructor patterns are not supported yet" + } +} diff --git a/src/compiler/internal/transformer/statements.gleam b/src/compiler/internal/transformer/statements.gleam index c907b8e..fdaff83 100644 --- a/src/compiler/internal/transformer/statements.gleam +++ b/src/compiler/internal/transformer/statements.gleam @@ -1,4 +1,5 @@ import compiler/internal/transformer as internal +import compiler/internal/transformer/patterns import compiler/python import glance import gleam/int @@ -23,6 +24,7 @@ pub fn transform_statement_block( context: internal.TransformerContext( next_function_id: 0, next_block_id: 0, + next_case_id: 0, ), statements: [], ), @@ -145,7 +147,7 @@ fn transform_expression( glance.Block(statements) -> transform_block(context, statements) - glance.Case(..) -> todo as "case expressions not supported yet" + glance.Case(subjects, clauses) -> transform_case(context, subjects, clauses) glance.TupleIndex(tuple, index) -> { transform_expression(context, tuple) @@ -340,7 +342,7 @@ fn transform_fn( fn transform_block( context: internal.TransformerContext, body: List(glance.Statement), -) { +) -> internal.ExpressionReturn { let function_name = "_fn_block_" <> int.to_string(context.next_block_id) let function = python.Function(function_name, [], transform_statement_block(body)) @@ -354,6 +356,70 @@ fn transform_block( ) } +fn transform_case( + context: internal.TransformerContext, + subjects: List(glance.Expression), + clauses: List(glance.Clause), +) -> internal.ExpressionReturn { + let subjects_result = case subjects { + [] -> panic("No subjects!") + [subject] -> transform_expression(context, subject) + multiple -> transform_tuple(context, multiple) + } + let clause_result = + list.fold( + clauses, + internal.TransformState(subjects_result.context, [], []), + fold_case_clause, + ) + + let function_name = "_fn_case_" <> int.to_string(context.next_case_id) + let function = + python.Function(function_name, [python.NameParam("_case_subject")], [ + python.Match(clause_result.item |> list.reverse), + ]) + + internal.ExpressionReturn( + context: internal.TransformerContext( + ..subjects_result.context, + next_case_id: context.next_case_id + 1, + ), + statements: list.append(subjects_result.statements, [ + python.FunctionDef(function), + ]), + expression: python.Call(python.Variable(function_name), [ + python.UnlabelledField(subjects_result.expression), + ]), + ) +} + +fn fold_case_clause( + state: internal.TransformState(internal.ReversedList(python.MatchCase)), + clause: glance.Clause, +) -> internal.TransformState(internal.ReversedList(python.MatchCase)) { + case clause { + glance.Clause(guard: option.Some(_), ..) -> + todo as "Case guards not implemented yet" + + glance.Clause([pattern_list], option.None, glance.Block(_statements)) -> { + todo as "block case clauses not supported yet" + } + + glance.Clause(pattern_list, option.None, body) -> { + let python_pattern = patterns.transform_alternative_patterns(pattern_list) + let body_result = transform_expression(state.context, body) + + internal.merge_state_prepend(state, body_result, fn(expr) { + python.MatchCase(python_pattern, [python.Return(expr)]) + }) + } + + glance.Clause(..) -> { + todo as "multiple clause not implemented yet" + } + } +} + fn transform_pipe( context: internal.TransformerContext, left: glance.Expression, diff --git a/src/compiler/python.gleam b/src/compiler/python.gleam index ab4722d..e6f6111 100644 --- a/src/compiler/python.gleam +++ b/src/compiler/python.gleam @@ -55,6 +55,7 @@ pub type Statement { Expression(Expression) Return(Expression) FunctionDef(Function) + Match(cases: List(MatchCase)) SimpleAssignment(name: String, value: Expression) } @@ -85,6 +86,23 @@ pub type CustomType { CustomType(name: String, parameters: List(String), variants: List(Variant)) } +pub type MatchCase { + // Inner List is collecting tuples together, outer list is patterns that get or'd together + // e.g. 1,2 | 3, 4 becomes [[1,2], [3, 4]] + MatchCase(pattern: Pattern, body: List(Statement)) +} + +pub type Pattern { + PatternWildcard + PatternInt(value: String) + PatternFloat(value: String) + PatternString(value: String) + PatternVariable(value: String) + PatternAssignment(pattern: Pattern, name: String) + PatternTuple(value: List(Pattern)) + PatternAlternate(patterns: List(Pattern)) +} + pub type Function { Function( name: String, diff --git a/test/case_test.gleam b/test/case_test.gleam new file mode 100644 index 0000000..b2fa39f --- /dev/null +++ b/test/case_test.gleam @@ -0,0 +1,191 @@ +import compiler +import gleeunit/should + +pub fn single_int_case_test() { + "pub fn main() { + case 1 { + 1 -> \"one\" + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case 1: + return \"one\" + return _fn_case_0(1)", + ) +} + +pub fn single_float_case_test() { + "pub fn main() { + case 1.0 { + 1.0 -> \"one\" + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case 1.0: + return \"one\" + return _fn_case_0(1.0)", + ) +} + +pub fn single_string_case_test() { + "pub fn main() { + case \"hello\" { + \"hello\" -> \"one\" + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case \"hello\": + return \"one\" + return _fn_case_0(\"hello\")", + ) +} + +pub fn variable_case_test() { + "pub fn main() { + case \"hello\" { + greet -> greet <> \" world\" + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case greet: + return greet + \" world\" + return _fn_case_0(\"hello\")", + ) +} + +pub fn tuple_case_test() { + "pub fn main() { + case #(1, 2) { + #(1, 2) -> \"one\" + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case (1, 2): + return \"one\" + return _fn_case_0((1, 2))", + ) +} + +pub fn pattern_assignment_test() { + "pub fn main() { + case 1 { + 1 as x -> 2 + x + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case 1 as x: + return 2 + x + return _fn_case_0(1)", + ) +} + +pub fn grouped_pattern_test() { + "pub fn main() { + case 1, 2 { + 1, x -> x + 50 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case (1, x): + return x + 50 + return _fn_case_0((1, 2))", + ) +} + +pub fn alternate_pattern_test() { + "pub fn main() { + case 1 { + 1 | 2 -> 5 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case 1 | 2: + return 5 + return _fn_case_0(1)", + ) +} + +pub fn alternate_grouped_pattern_test() { + "pub fn main() { + case 1, 2 { + 1, 2 | 2, 3 -> 5 + } + } + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +def main(): + def _fn_case_0(_case_subject): + match _case_subject: + case (1, 2) | (2, 3): + return 5 + return _fn_case_0((1, 2))", + ) +}