Skip to content

Latest commit

 

History

History
371 lines (292 loc) · 6.79 KB

kombucha.livemd

File metadata and controls

371 lines (292 loc) · 6.79 KB

🍹The Kombucha Programming Language

Mix.install([
  {:kino, "~> 0.10.0"}
])
:ok

Kombucha Transpiler

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

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

Lambdas are defined by the ${ keyword. And later called by using $.

"""
heyho = ${ message ->
  to_string(message)
  |> String.upcase()
}
$heyho(:letsgo)
"""
|> Kombucha.eval()
"LETSGO"

Tuples

Tuples and Maps needs a semicolon at the end to differentiate from "end" blocks

"""
{:ok, 1234};
"""
|> Kombucha.eval()
{:ok, 1234}

Maps

"""
module City {
  pub map(city) {
    %{city: city};
  }
}

City.map(:santiago)
"""
|> Kombucha.eval()
%{city: :santiago}

Const

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

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

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]