macabre : (adj) tending to produce horror in a beholder
This is an ultra experimental compiler written in Gleam to compile Gleam source code (using the glance package) to Python.
It covers a pretty decent swath of Gleam syntax, and I made a point of prioritizing the hardest syntax. There is a big pile of TODOs at the end of this file if you want to contribute.
My current vision is to have fun while making this a self-hosted compiler. Once it can compile itself to Python (bonus points if it runs on a variation of gleam_otp that uses Python's upcoming new experimental multi-threaded support), well at that point, I'll have to come up with a new vision. Maybe something to do with supporting other compilation targets, such as Gleam's native Erlang and Javascript environments.
Probability of achieving this vision: pretty low. This is a for-fun free time project for me. I'm currently on sabbatical so I have time for odd little projects like this.
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:
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:
name = "example"
[dependencies]
macabre_stdlib = "git@github.com: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:
python build/dev/python
Run tests with gleeunit:
gleam test
Run tests in watch mode:
fd .gleam | entr gleam test
PRs are welcome.
The main entry point is macabre.gleam
, which handles all file loading and
other side effects.
The package depends on the glance AST parser, and glimpse, a package I wrote to wrap glance to support multiple inter-dependent modules. (Eventually I want it to also be a glance typechecker)
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.
The Python AST is in python.gleam
. This doesn't model all of python; just the
subset that is needed to map Gleam expressions to.
Some tasks below are marked easy if you want to get started.
def
right on the class or have the def
be defined somewhere and just attach it like other fields__all__
fn foo(bar baz: Str)
)io.println
Foo(mystr: String, point: #(Int, Int))
can be called with Foo(#(1, 1), mystr: "Foo")
in gleam