Skip to content

Commit

Permalink
Oh hey, dependencies.
Browse files Browse the repository at this point in the history
  • Loading branch information
dusty-phillips committed Aug 28, 2024
1 parent 87fde3a commit 1de6b81
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 121 deletions.
74 changes: 63 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,66 @@ I have no idea. It's fun and educational. (I'm on a roll with this redefining wo

## Usage

The complier runs on gleam erlang successfully. I think it might work with
Javascript as I don't think I'm using any erlang-specific libraries yet. It
can't compile itself to python yet, but I'm hoping to get there.

To run it, pass it a properly structured macabre folder as the only argument:

```sh
gleam run -- some_very_simple_file.gleam # compile to python
gleam run -- some_package_folder
```

The package folder should have a structure similar to a normal Gleam project:

```
folder
├── gleam.toml
├── build
│ └── <generated stuff>
└─ src
├── <repo_name>.gleam
├── some_folder
│ ├── something.gleam
│ └── bindings.py
├── some_file.gleam
└── some_bindings.py
```

The `gleam.toml` only supports two keys, name and dependencies:

```toml
name = "example"

[dependencies]
macabre_stdlib = "[email protected]:dusty-phillips/macabre_stdlib.git"
```

Note that dependencies are currently _not_ hex packages like a normal gleam
project. Rather, they are git repositories. This is mostly because I didn't
feel comfortable cluttering hex with silly dependencies for my silly project.

The compiler expects git to be installed, and will clone any git repos that are
listed.

> [!WARNING} It currently downloads everything from scratch every time you
> invoke it, so don't try this on a metered connection!
Macabre copies the `src/` folder of each package into the build directory and
then builds all dependencies from scratch (every single time). Your source
files are also copied into this folder.

Your main module will always be `<repo_name>.gleam` where `<repo_name>` is whatever
you put in the `name` in `gleam.toml`.

Your files are compiled to `build/dev/python`. If your `<repo_name>.gleam` has
a `main` function in it, then the compiler will generate a
`build/dev/python/__main__.py` to call that function.

Use this command to invoke it:

```shell
python build/dev/python
```

## Development
Expand All @@ -39,14 +97,12 @@ fd .gleam | entr gleam test

## Contributing

Are you insane?

Sweet, me too.

PRs are welcome.

The main entry point is `macabre.gleam`, which handles all file loading and
other side effects. The compiler is pure gleam. Most of the work happens in
other side effects.

The compiler is pure gleam. Most of the work happens in
`transformer.gleam` and `generator.gleam`. The former converts the Gleam AST to
a Python AST, the latter generates python code. There are tons of helper
functions in various other files.
Expand All @@ -61,6 +117,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.

- (EASY) Turn this list into github issues
- imports with attributes are not handled yet
- type imports are not implemented yet
- Unlabelled fields in custom types are not generated yet
Expand Down Expand Up @@ -100,12 +157,7 @@ are ) in the codebase, as of the last time that I updated this list.
- glance doesn't have (much of) a typechecker
- not currently generating python type hints (e.g. function arguments and
return types), but gleam gives us that info so may as well use it
- no concept of a "project", gleam.toml, downloading dependencies
- only compiles one module at a time, doesn't follow imports
- copies the prelude module blindly into the directory that contains that one module instead of a top level
- No standard library
- No Result custom type yet (I thought this needed to be in the prelude, but I don't see any result-specific syntax anywhere)
- generate `__main__.py` if a module has a main function
- calling functions or constructors with out-of-order positional args doesn't
work in python
- e.g. `Foo(mystr: String, point: #(Int, Int))` can be called with `Foo(#(1,
Expand Down
2 changes: 2 additions & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ argv = ">= 1.0.2 and < 2.0.0"
simplifile = ">= 2.0.1 and < 3.0.0"
filepath = ">= 1.0.0 and < 2.0.0"
glexer = ">= 1.0.1 and < 2.0.0"
tom = ">= 1.0.1 and < 2.0.0"
shellout = ">= 1.6.0 and < 2.0.0"

[dev-dependencies]
gleescript = ">= 1.4.0 and < 2.0.0"
Expand Down
3 changes: 3 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ packages = [
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
{ name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" },
{ name = "pprint", version = "1.0.3", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "76BBB92E23D12D954BD452686543F29EDE8EBEBB7FC0ACCBCA66EEF276EC3A06" },
{ name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" },
{ name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" },
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
{ name = "temporary", version = "1.0.0", build_tools = ["gleam"], requirements = ["envoy", "exception", "filepath", "gleam_crypto", "gleam_stdlib", "simplifile"], otp_app = "temporary", source = "hex", outer_checksum = "51C0FEF4D72CE7CA507BD188B21C1F00695B3D5B09D7DFE38240BFD3A8E1E9B3" },
Expand All @@ -30,5 +31,7 @@ gleescript = { version = ">= 1.4.0 and < 2.0.0" }
gleeunit = { version = ">= 1.2.0 and < 2.0.0" }
glexer = { version = ">= 1.0.1 and < 2.0.0" }
pprint = { version = ">= 1.0.3 and < 2.0.0" }
shellout = { version = ">= 1.6.0 and < 2.0.0" }
simplifile = { version = ">= 2.0.1 and < 3.0.0" }
temporary = { version = ">= 1.0.0 and < 2.0.0" }
tom = { version = ">= 1.0.1 and < 2.0.0" }
11 changes: 5 additions & 6 deletions src/compiler.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import compiler/transformer
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import pprint

pub fn compile_module(glance_module: glance.Module) -> String {
glance_module
Expand All @@ -15,12 +14,12 @@ pub fn compile_module(glance_module: glance.Module) -> String {
}

pub fn compile_package(package: package.GleamPackage) -> package.CompiledPackage {
pprint.debug(package.modules)
package.CompiledPackage(
base_directory: package.base_directory,
main_module: dict.get(package.modules, package.main_module)
project: package.project,
has_main: dict.get(package.modules, package.project.name <> ".gleam")
|> result.try(fn(mod) { mod.functions |> has_main_function })
|> result.replace(package.main_module |> string.drop_right(6))
|> option.from_result,
|> result.is_ok,
modules: package.modules
|> dict.map_values(fn(_key, value) { compile_module(value) }),
external_import_files: package.external_import_files,
Expand Down
58 changes: 15 additions & 43 deletions src/compiler/package.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,45 @@
//// filesystem.
//// It does not write to the filesystem.

import compiler/project
import errors
import filepath
import filesystem
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import gleam/set
import gleam/string
import simplifile

pub type GleamPackage {
GleamPackage(
base_directory: String,
main_module: String,
project: project.Project,
modules: dict.Dict(String, glance.Module),
external_import_files: set.Set(String),
)
}

pub type CompiledPackage {
CompiledPackage(
base_directory: String,
main_module: option.Option(String),
project: project.Project,
has_main: Bool,
modules: dict.Dict(String, String),
external_import_files: set.Set(String),
)
}

/// Load the entry_point file and recursively load and parse any modules it
///returns.
pub fn load_package(
source_directory: String,
pub fn load(
gleam_project: project.Project,
) -> Result(GleamPackage, errors.Error) {
source_directory
|> simplifile.is_directory
|> result.map_error(errors.FileOrDirectoryNotFound(source_directory, _))
|> result.try(fn(_) { find_entrypoint(source_directory) })
|> result.try(fn(entrypoint) {
load_module(
GleamPackage(
source_directory,
entrypoint,
dict.new(),
external_import_files: set.new(),
),
entrypoint,
)
})
}

pub fn find_entrypoint(source_directory: String) -> Result(String, errors.Error) {
let base_name = filepath.base_name(source_directory)
let entrypoint = base_name <> ".gleam"
simplifile.is_file(filepath.join(source_directory, entrypoint))
|> result.replace(entrypoint)
|> result.map_error(errors.FileOrDirectoryNotFound(entrypoint, _))
}

pub fn source_directory(base_directory: String) -> String {
filepath.join(base_directory, "src")
}

pub fn build_directory(base_directory: String) -> String {
filepath.join(base_directory, "build")
use _ <- result.try(filesystem.is_directory(project.src_dir(gleam_project)))
use package <- result.try(load_module(
GleamPackage(gleam_project, dict.new(), external_import_files: set.new()),
project.entry_point(gleam_project),
))
Ok(package)
}

/// Parse the module and add it to the package's modules, if it can be parsed.
Expand All @@ -82,11 +56,9 @@ fn load_module(
Ok(_) -> Ok(package)
Error(_) -> {
let module_result =
package.base_directory
|> source_directory
project.build_src_dir(package.project)
|> filepath.join(module_path)
|> simplifile.read
|> result.map_error(errors.FileReadError(module_path, _))
|> filesystem.read
|> result.try(parse(_, module_path))

case module_result {
Expand Down
Loading

0 comments on commit 1de6b81

Please sign in to comment.