From 91152a3c6ab8abb6fa298c6fbb7e851803c25f5e Mon Sep 17 00:00:00 2001 From: Dusty Phillips Date: Sat, 24 Aug 2024 09:13:19 -0300 Subject: [PATCH] Process imports better --- README.md | 3 +- src/compiler/internal/generator/imports.gleam | 19 ++- src/compiler/internal/transformer.gleam | 12 -- src/compiler/python.gleam | 13 +- src/compiler/transformer.gleam | 89 ++++++++++++- test/imports_test.gleam | 125 ++++++++++++++++++ 6 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 test/imports_test.gleam diff --git a/README.md b/README.md index b563594..41226b2 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ 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. -- (EASY) Aliased imports are not generated yet +- imports with attributes are not implemented 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 a feeling we have to ditch dataclasses. Probably a custom class with slots, a diff --git a/src/compiler/internal/generator/imports.gleam b/src/compiler/internal/generator/imports.gleam index 08e1fb4..46c8869 100644 --- a/src/compiler/internal/generator/imports.gleam +++ b/src/compiler/internal/generator/imports.gleam @@ -1,7 +1,7 @@ import compiler/internal/generator as internal import compiler/python -import gleam/option import gleam/string_builder.{type StringBuilder} +import pprint pub fn generate_imports(imports: List(python.Import)) -> StringBuilder { internal.generate_plural(imports, generate_import, "\n") @@ -10,13 +10,24 @@ pub fn generate_imports(imports: List(python.Import)) -> StringBuilder { fn generate_import(import_: python.Import) -> StringBuilder { case import_ { - python.UnqualifiedImport(module, name, option.None) -> + python.QualifiedImport(module) -> + string_builder.from_strings(["import ", module]) + python.AliasedQualifiedImport(module, alias) -> + string_builder.from_strings(["import ", module, " as ", alias]) + python.UnqualifiedImport(module, name) -> string_builder.new() |> string_builder.append("from ") |> string_builder.append(module) |> string_builder.append(" import ") |> string_builder.append(name) - python.UnqualifiedImport(_module, _name, option.Some(_)) -> - todo as "Aliased imports not supported yet" + python.AliasedUnqualifiedImport(module, name, alias) -> { + string_builder.new() + |> string_builder.append("from ") + |> string_builder.append(module) + |> string_builder.append(" import ") + |> string_builder.append(name) + |> string_builder.append(" as ") + |> string_builder.append(alias) + } } } diff --git a/src/compiler/internal/transformer.gleam b/src/compiler/internal/transformer.gleam index 7c4eef8..71fd636 100644 --- a/src/compiler/internal/transformer.gleam +++ b/src/compiler/internal/transformer.gleam @@ -117,18 +117,6 @@ pub fn reverse_state_to_return( ) } -pub fn maybe_extract_external( - function_attribute: glance.Attribute, -) -> Result(python.Import, TransformError) { - case function_attribute { - glance.Attribute( - "external", - [glance.Variable("python"), glance.String(module), glance.String(name)], - ) -> Ok(python.UnqualifiedImport(module, name, option.None)) - _ -> Error(NotExternal) - } -} - pub fn transform_last(elements: List(a), transformer: fn(a) -> a) -> List(a) { // This makes three iterations over elements. It may be a candidate for optimization // since it happens on all the statements in every function body. I can find ways diff --git a/src/compiler/python.gleam b/src/compiler/python.gleam index 12983aa..a2af3e8 100644 --- a/src/compiler/python.gleam +++ b/src/compiler/python.gleam @@ -4,16 +4,15 @@ pub type Context(a) { Context(imports: List(Import), item: a) } +// TODO: for my sanity, I didn't group imports as in "from x import (a, b)" +// We can either fix this or rely on a import formatter on the exported code pub type Import { - UnqualifiedImport(module: String, name: String, alias: option.Option(String)) + UnqualifiedImport(module: String, name: String) + AliasedUnqualifiedImport(module: String, name: String, alias: String) + QualifiedImport(module: String) + AliasedQualifiedImport(module: String, alias: String) } -pub const dataclass_import = UnqualifiedImport( - "dataclasses", - "dataclass", - option.None, -) - pub type BinaryOperator { And Or diff --git a/src/compiler/transformer.gleam b/src/compiler/transformer.gleam index 0e2739e..72309c7 100644 --- a/src/compiler/transformer.gleam +++ b/src/compiler/transformer.gleam @@ -4,9 +4,13 @@ import compiler/internal/transformer/types import compiler/python import glance import gleam/list +import gleam/option +import gleam/string +import pprint pub fn transform(input: glance.Module) -> Result(python.Module, String) { python.empty_module() + |> list.fold(input.imports, _, transform_import) |> list.fold(input.functions, _, transform_function_or_external) |> list.fold(input.custom_types, _, transform_custom_type_in_module) |> Ok @@ -16,7 +20,7 @@ fn transform_function_or_external( module: python.Module, function: glance.Definition(glance.Function), ) -> python.Module { - case list.filter_map(function.attributes, internal.maybe_extract_external) { + case list.filter_map(function.attributes, maybe_extract_external) { [] -> python.Module( ..module, @@ -31,6 +35,77 @@ fn transform_function_or_external( } } +fn transform_import( + module: python.Module, + import_: glance.Definition(glance.Import), +) -> python.Module { + let python_imports = case import_ { + glance.Definition(attributes: [_head, ..], ..) -> + todo as "import attributes not supported yet" + + glance.Definition([], glance.Import(_, _, [_head, ..], _)) -> { + todo as "type alias imports not supported yet" + } + + glance.Definition([], glance.Import(module, alias, [], unqualified_values)) -> { + let module_import = transform_module_import(module, alias) + let module_part = + module + |> string.replace("/", ".") + + unqualified_values + |> list.map(transform_unqualified_description(_, module_part)) + |> list.prepend(module_import) + } + } + python.Module(..module, imports: list.append(module.imports, python_imports)) +} + +fn transform_module_import( + module: String, + alias: option.Option(glance.AssignmentName), +) -> python.Import { + let #(build_qual, build_unqual) = case alias { + option.None -> #(python.QualifiedImport, python.UnqualifiedImport) + option.Some(assignment_name) -> #( + python.AliasedQualifiedImport(_, transform_import_alias(assignment_name)), + fn(mod, name) { + python.AliasedUnqualifiedImport( + mod, + name, + transform_import_alias(assignment_name), + ) + }, + ) + } + + case module |> string.split("/") |> list.reverse { + [] -> panic as "Expected at least one module import" + [module] -> build_qual(module) + [last_module, ..modules] -> + build_unqual(modules |> list.reverse |> string.join("."), last_module) + } +} + +fn transform_import_alias(assignment: glance.AssignmentName) -> String { + // todo: may need some mapping on discarded names + case assignment { + glance.Named(string) -> string + glance.Discarded(string) -> string + } +} + +fn transform_unqualified_description( + unqual: glance.UnqualifiedImport, + module: String, +) -> python.Import { + case unqual.alias { + option.None -> python.UnqualifiedImport(module, unqual.name) + option.Some(alias) -> + python.AliasedUnqualifiedImport(module, unqual.name, alias) + } +} + fn transform_custom_type_in_module( module: python.Module, custom_type: glance.Definition(glance.CustomType), @@ -43,3 +118,15 @@ fn transform_custom_type_in_module( ], ) } + +pub fn maybe_extract_external( + function_attribute: glance.Attribute, +) -> Result(python.Import, internal.TransformError) { + case function_attribute { + glance.Attribute( + "external", + [glance.Variable("python"), glance.String(module), glance.String(name)], + ) -> Ok(python.UnqualifiedImport(module, name)) + _ -> Error(internal.NotExternal) + } +} diff --git a/test/imports_test.gleam b/test/imports_test.gleam new file mode 100644 index 0000000..bff5d31 --- /dev/null +++ b/test/imports_test.gleam @@ -0,0 +1,125 @@ +import compiler +import gleeunit/should + +pub fn qualified_import_no_namespace_test() { + "import my_cool_lib" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +import my_cool_lib + + +", + ) +} + +pub fn qualified_aliased_import_no_namespace_test() { + "import my_cool_lib as thing" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +import my_cool_lib as thing + + +", + ) +} + +pub fn qualified_import_namespaces_test() { + "import my/cool/lib" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +from my.cool import lib + + +", + ) +} + +pub fn qualified_aliased_import_namespaces_test() { + "import my/cool/lib as thing" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +from my.cool import lib as thing + + +", + ) +} + +pub fn unqualified_import_test() { + "import my_cool_lib.{hello}" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +import my_cool_lib +from my_cool_lib import hello + + +", + ) +} + +pub fn unqualified_import_namespace_test() { + "import my/cool/lib.{hello}" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +from my.cool import lib +from my.cool.lib import hello + + +", + ) +} + +pub fn unqualified_import_aliased_test() { + "import my/cool/lib.{hello as foo, world as bar}" + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +from my.cool import lib +from my.cool.lib import hello as foo +from my.cool.lib import world as bar + + +", + ) +} + +pub fn aliased_modules_with_quals_test() { + "import my/cool/lib.{hello as foo, world} as notlib + import something.{hello as baz, continent} as nothing + " + |> compiler.compile + |> should.be_ok + |> should.equal( + "from gleam_builtins import * + +import something as nothing +from something import hello as baz +from something import continent +from my.cool import lib as notlib +from my.cool.lib import hello as foo +from my.cool.lib import world + + +", + ) +}