Skip to content

Commit

Permalink
Process imports better
Browse files Browse the repository at this point in the history
  • Loading branch information
dusty-phillips committed Aug 24, 2024
1 parent 55c5e82 commit 91152a3
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 25 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions src/compiler/internal/generator/imports.gleam
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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)
}
}
}
12 changes: 0 additions & 12 deletions src/compiler/internal/transformer.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions src/compiler/python.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 88 additions & 1 deletion src/compiler/transformer.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -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)
}
}
125 changes: 125 additions & 0 deletions test/imports_test.gleam
Original file line number Diff line number Diff line change
@@ -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
",
)
}

0 comments on commit 91152a3

Please sign in to comment.