Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/rwillians/rinha-de-compilers--elixir-transcompiler
Source-to-Source Transcompiler from `.rinha` to Elixir. Runs on ERTS (Erlang Runtime System).
https://github.com/rwillians/rinha-de-compilers--elixir-transcompiler
elixir rinha-de-compilers
Last synced: 5 days ago
JSON representation
Source-to-Source Transcompiler from `.rinha` to Elixir. Runs on ERTS (Erlang Runtime System).
- Host: GitHub
- URL: https://github.com/rwillians/rinha-de-compilers--elixir-transcompiler
- Owner: rwillians
- License: mit
- Created: 2023-09-07T21:30:35.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2023-09-25T20:51:30.000Z (about 1 year ago)
- Last Synced: 2023-09-27T01:56:09.354Z (about 1 year ago)
- Topics: elixir, rinha-de-compilers
- Language: Elixir
- Homepage:
- Size: 93.8 KB
- Stars: 5
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.bkp.md
- License: LICENSE
Awesome Lists containing this project
README
---
# A Source-to-Source Transcompiler
The core idea here is to use Elixir (at compile time) to parse a `.rinha` program, transpile it to Elixir AST and then compile it as an Elixir program.
## How to use it?
You just need to create a module where your transpiled `rinha` program will live. To transpcompile the code, all you gotta do is use the `Transcompile` module:
```elixir
# lib/rinha/fib.exdefmodule Rinha.Fib do
use Transcompiler,
source: {:file, path: ".rinha/files/fib.rinha"},
parser: Rinha.Parser
end```
All functions defined in your `.rinha` program file will be extracted from the syntax tree then transpiled as Elixir's `def` functions (public module functions). That's necessary to allow for recursive functions. As for the rest of the tree, all script-like procedural code will be transpiled into a `main/0` public function in the same module.
## Running it
> **Note**
> I assume you have `asdf-vm` installed (because you should 👀 -- it's like nvm, but for anything basically).1. Clone the repo (yes, that `--recursive` flag is important):
```sh
git clone --recursive [email protected]:rwillians/rinha-de-compiladores.git
```2. Install Elixir and Erlang with the versions specified in the file `.tool-versions`:
```sh
asdf install
```3. Install dependencies:
```sh
mix deps.get
```4. Compile dependencies (shouldn't be timmed):
```sh
mix deps.compile
```5. Compile the main source code (that's the one you want to time):
```sh
mix compile
```### Run any `.rinha` program:
```sh
mix run play.exs "./relative/path/to/program.rinha"
```To compile once then execute `n` times:
```sh
mix run play.exs "./relative/path/to/program.rinha" 1000
```And, if you're running that many times you'll probably want to redirect `stdout` to `/dev/null`:
```sh
mix run play.exs "./relative/path/to/program.rinha" 1000000 &>/dev/null
```### Run using the REPL:
```sh
iex -S mix
```Call whatever function you'd like to see working:
```elixir
Rinha.Fib.main()
```Note that functions specified in the program are public functions, meaning you could call `fib/1` from the REPL as well:
```elixir
Rinha.Fib.fib(15)
```You can also play with the other test programs:
```elixir
Rinha.Combination.main()
``````elixir
Rinha.Sum.main()
```## How does it work?
Let's take `Rinha.Fib` as an example:
```elixir
# lib/rinha/fib.exdefmodule Rinha.Fib do
use Transcompiler,
source: {:file, path: ".rinha/files/fib.rinha"},
parser: Rinha.Parser
end
```When you use `use Transcompiler`, we first take that `path` to the `fib.rinha` program and make sure it's associated with your módule (e.g.: `Rinha.Fib`) so that, whenever `fib.rinha` is changed, then the módulo is recompiled:
```elixir
# lib/transcompiler.exdefmodule Transcompiler do
# ...defmacro __using__(opts) do
# ...quote do
@external_resource unquote(path)# ...
end
end# ...
end
```Then the contents of `fib.rinha` is read and parsed into a generic AST:
```elixir
# lib/transcompiler.exdefmodule Transcompiler do
#...defmacro __using__(opts) do
# ...quote do
# ...ast = File.read!(unquote(path))
# ^ read the contents of the file|> unquote(parser).parse(unquote(path))
# ^ calls function `parse/2` from the `parser`
# given when using `use Transcompiler`# |> Ex.Tuple.unwrap!()
# |> unquote(__MODULE__).transpile(__MODULE__)# ...
end
end# ...
end
```The parser is implemented using [NimbleParsed](https://github.com/dashbitco/nimble_parsec) for parser combinators that are compiled to functions where rules are choosen via pattern matching (meaning, it's fast!):
```elixir
# lib/rinha/parser.exdefmodule Rinha.Parser do
# ...defparsec :bool,
choice([
string("true") |> replace(true),
string("false") |> replace(false)
])
|> unwrap_and_tag(:boolean)defparsecp :let,
ignore(string("let"))
|> ignore(times(space, min: 1))
|> parsec(:var)
|> ignore(times(space, min: 1))
|> ignore(string("="))
|> ignore(times(space, min: 1))
|> unwrap_and_tag(parsec(:expr), :value)
|> ignore(optional(string(";")))
|> tag(:let)defparsecp :binary_op,
empty()
|> unwrap_and_tag(parsec(:term), :lhs)
|> ignore(times(space, min: 1))
|> unwrap_and_tag(parsec(:operator), :op)
|> ignore(times(space, min: 1))
|> unwrap_and_tag(parsec(:term), :rhs)
|> tag(:binary_op)# ...
def parse(program, filename) do
with {:ok, [{:file, exprs}], "", _, _, _} <- file(program),
do: {:ok, to_common_ast({:file, exprs}, %{filename: filename})}
end# ...
defp to_common_ast({:string, value}, ctx) do
%Transcompiler.String{
value: value,
location: %Transcompiler.Location{filename: ctx.filename}
}
end
end
```Parsing a program results in a generic AST like this:
```elixir
# "let a = k == 0;"%Transcompiler.File{
name: "foo.rinha",
block: [
%Transcompiler.Let{
var: %Transcompiler.Variable{name: :a, location: %Transcompiler.Location{}},
value: %Transcompiler.BinaryOp.Eq{
lhs: %Transcompiler.Variable{name: :k, location: %Transcompiler.Location{}},
rhs: %Transcompiler.Integer{value: 0, location: %Transcompiler.Location{}},
location: %Transcompiler.Location{}
},
location: %Transcompiler.Location{}
}
],
location: %Transcompiler.Location{}
}
```There's a total of 27 types of tokens that can be composed into that generic AST. Each token implements a `Transpiler` protocol, which introduces the function `to_elixir_ast` that is capable of taking a specific type of AST node and recursively transpile it to Elixir AST.
```elixir
# lib/transcompiler/transpiler.exdefprotocol Transcompiler.Transpiler do
@spec to_elixir_ast(ast :: struct, env :: module) :: Macro.t()
def to_elixir_ast(ast, env)
end
``````elixir
# lib/transcompiler/binary_op.add.exdefimpl Transcompiler.Transpiler, for: Transcompiler.BinaryOp.Add do
def to_elixir_ast(ast, env) do
{:+, [context: env, imports: [{1, Kernel}, {2, Kernel}]], [
Transcompiler.Transpiler.to_elixir_ast(ast.lhs, env),
Transcompiler.Transpiler.to_elixir_ast(ast.rhs, env),
]}
end
end
```Now that we have a generic AST and that we're capable of transpiling it to Elixir AST, let's get back to `Transcompiler` module. It takes the generic AST and transpiles it to Elixir AST then apply such AST to the module which invoked `use Transcompile`:
```elixir
# lib/transcompiler.exdefmodule Transcompiler do
# ...defmacro __using__(opts) do
# ...quote do
# ...ast = File.read!(unquote(path))
# ^ read the contents for `fib.rinha`|> unquote(parser).parse(unquote(path))
# ^ parses into generic AST|> Ex.Tuple.unwrap!()
# ^ raises and error if something goes wrong
# with parsing|> unquote(__MODULE__).transpile(__MODULE__)
# ^ recursively transpiles generic
# AST into Elixir ASTModule.eval_quoted(__MODULE__, ast)
# ^ applies Elixir AST to the module which invoked
# `use Transcompiler`
end
end# ...
end
```And that's it. Now the `.rinha` code is Elixir code; get's compiled as Elixir code; and runs as any Elixir code.
## Where to find me
| Name | Link |
|----------:|:-----------------------------------------------------|
| 𝕏 Twitter | [@rwillians_](https://twitter.com/rwillians_) |
| LinkedIn | [@rwillians](https://www.linkedin.com/in/rwillians/) |
| GitHub | [@rwillians](https://github.com/rwillians) |
| Resume | [rwillians.com](https://rwillians.com/resume) |## What's next?
- [ ] `#debug` add line number and character offset to `Transcompiler.Location` on all tokens;
- [ ] `#improvement` support functions to be declared anywhere in the file, not only at the root scope;
- [ ] `#dx` `#debug` provide nicer error messages when parsing fails.