Mix.install([
{:kino, "~> 0.10.0"}
])
:ok
Kombucha is a experimental programming language. A combination between Elixir and Gleam.
defmodule Kombucha do
defp peek(tokens, count \\ 1) do
Enum.take(tokens, count)
end
defp next([]) do
{[], []}
end
defp next(tokens) do
{peek(tokens), Enum.drop(tokens, 1)}
end
defp next_token(tokens) do
case next(tokens) do
{[], []} -> {{:eof, nil}, []}
{[item], remaining_tokens} -> {item, remaining_tokens}
end
end
defp process_const(acc, _rest, tokens) do
# Transform const to a public function
# const my_const {
# "value"
# }
# def my_const do
# "value"
# end
#
# const my_const = 1324
# def my_const, do: 1234
{{name, _index}, remain} = next_token(tokens)
{{value, _index}, remain} = next_token(remain)
case value do
"=" <> _rest -> {acc ++ ["def " <> String.trim(name) <> ", do: "], remain}
value -> {acc ++ ["def " <> String.trim(name) <> " " <> value], remain}
end
end
def process_comment(acc, tokens) do
{token, remain} = next_token(tokens)
case token do
{:eof, _} ->
{acc, []}
# Go back to the main flow
{"__comment_block_end__" <> _rest, _} ->
{acc, remain}
# Keep consuming tokens until block end or eof
_any ->
process_comment(acc, remain)
end
end
defp process_struct(acc, tokens) do
# Transform structs to the elixir format
{{name, _}, remain} = next_token(tokens)
{{_do, _}, remain} = next_token(remain)
{{_newline, _}, remain} = next_token(remain)
{{fields, _}, remain} = next_token(remain)
{acc ++
[
"""
defmodule #{String.trim(name)} do
alias __MODULE__
defstruct #{String.trim(fields)}
"""
], remain}
end
defp process_lambda(acc, rest, tokens) do
{acc ++ [String.replace_trailing(rest, "(", ".(")], tokens}
end
defp transform(acc, []) do
acc
end
defp transform(acc, tokens) do
# Get the next token
{{token, _index}, remaining_tokens} = next_token(tokens)
# Return the accumulator and remaining tokens
{out, remaining_tokens} =
case token do
# Keywords
"module" <> rest -> {acc ++ ["defmodule" <> rest], remaining_tokens}
"macro" <> rest -> {acc ++ ["defmacrop" <> rest], remaining_tokens}
"guard" <> rest -> {acc ++ ["defguardp" <> rest], remaining_tokens}
"struct" <> _rest -> process_struct(acc, remaining_tokens)
"pub" <> rest -> {acc ++ ["def" <> rest], remaining_tokens}
"fun" <> rest -> {acc ++ ["defp" <> rest], remaining_tokens}
"$" <> rest -> process_lambda(acc, rest, remaining_tokens)
"const" <> rest -> process_const(acc, rest, remaining_tokens)
"__comment_block_start__" <> _rest -> process_comment(acc, remaining_tokens)
"};" <> rest -> {acc ++ ["}" <> rest], remaining_tokens}
# End the transpilation
token when token == :eof -> {acc, []}
# Store characters that are not keywords
token -> {acc ++ [token], remaining_tokens}
end
# Traverse the token tree until :eof
transform(out, remaining_tokens)
end
defp tokenize(input) do
input
|> String.trim()
|> String.replace("\r\n", "\n")
|> String.replace("\r", "\n")
|> String.replace("{\n", "do\n")
# lambda functions
|> String.replace("${", "fn")
|> String.replace("}\n", "end\n")
# for maps, structs
|> String.replace("};", " };")
# macros and guards
|> String.replace("pub macro", "defmacro ")
|> String.replace("pub guard", "defguard ")
# comments
|> String.replace("/*", " __comment_block_start__ ")
|> String.replace("*/", " __comment_block_end__ ")
|> String.replace("\n", " \n ")
|> String.split(~r/(?<=[()\s;=+\-*\/]|[()])/)
|> Enum.filter(&(&1 != "" && &1 != " "))
|> Enum.with_index()
end
def transpile(input) do
transform([], tokenize(input))
|> Enum.join()
end
def print(input) do
transpile(input)
|> Kino.Text.new()
end
def eval(input) do
transpile(input)
|> Code.eval_string()
|> then(fn {result, _} -> result end)
end
end
{:module, Kombucha, <<70, 79, 82, 49, 0, 0, 31, ...>>, {:eval, 1}}
Modules are defined by module
keyword and curly braces
"""
module Brew {
fun message() {
"Strong Kombucha"
}
pub strong() {
message()
}
}
Brew.strong()
"""
|> Kombucha.eval()
"Strong Kombucha"
Lambdas are defined by the ${
keyword. And later called by using $
.
"""
heyho = ${ message ->
to_string(message)
|> String.upcase()
}
$heyho(:letsgo)
"""
|> Kombucha.eval()
"LETSGO"
Tuples and Maps needs a semicolon at the end to differentiate from "end" blocks
"""
{:ok, 1234};
"""
|> Kombucha.eval()
{:ok, 1234}
"""
module City {
pub map(city) {
%{city: city};
}
}
City.map(:santiago)
"""
|> Kombucha.eval()
%{city: :santiago}
Const transpile down to public functions.
"""
module MyModule {
const myconst = 123
}
MyModule.myconst
"""
|> Kombucha.eval()
123
"""
module Bar {
# Also private consts as in elixir
@count 500
/*
Consts can also be used with blocks
*/
const drinks {
[:drink, @count, :kombucha]
}
}
Bar.drinks
"""
|> Kombucha.eval()
[:drink, 500, :kombucha]
Structs are modules that alias the __MODULE__
and uses the defstruct
"""
struct Glass {
~w[liquid]a
pub new() {
%Glass{liquid: :kombucha};
}
}
Glass.new()
"""
|> Kombucha.eval()
%Glass{liquid: :kombucha}
Macros and guards are private by default (using defmacrop
and defguardp
). To make them public use pub macro
and pub guard
.
"""
["macro": macro, "public_macro": pub macro, "guard": guard, "public_guard": pub guard]
"""
|> Kombucha.print()
"""
module MyInteger {
pub guard is_even(term) when is_integer(term) and rem(term, 2) == 0
pub even?(number) when is_even(number) {
:is_even
}
pub even?(_number) {
:not_even
}
}
[MyInteger.even?(2), MyInteger.even?(1)]
"""
|> Kombucha.eval()
[:is_even, :not_even]