Skip to content

Commit

Permalink
Add erlang-module support
Browse files Browse the repository at this point in the history
The code is deemed a module definition if the first line
start with "-module(".

In this case the entire code-block is interpreted as
erlang-module and if there are no errors the module is compiled
and loaded.

Basic error handling has been added
Basic test has been added ( TODO add failure cases )
  • Loading branch information
fnchooft committed Jul 21, 2023
1 parent 122cb67 commit 1447859
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 0 deletions.
92 changes: 92 additions & 0 deletions lib/livebook/runtime/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,99 @@ defmodule Livebook.Runtime.Evaluator do
{result, code_markers}
end

# main entry point to decide between erlang-statements vs module
defp eval(:erlang, code, binding, env) do
is_module = String.starts_with?(code, "-module(")

case is_module do
true -> eval_module(:erlang, code, binding, env)
false -> eval_statements(:erlang, code, binding, env)
end
end

# Simple Erlang Module - helper functions
# ------------------------------------------------------------------------
# In order to handle the expression in their forms - separate per {:dot,_}
defp not_dot({:dot, _}) do
false
end

defp not_dot(_) do
true
end

# A list of scanned token - must be seperated per dot, in order to feed them
# into the :erl_parse.parse_form function.
defp tokens_to_forms(tokens) do
{:ok, tokens_to_forms(tokens, [])}
end

defp tokens_to_forms([], acc) do
:lists.reverse(acc)
end

defp tokens_to_forms(tokens, acc) do
form = :lists.takewhile(&not_dot/1, tokens)
[dot | rest] = :lists.dropwhile(&not_dot/1, tokens)
tokens_to_forms(rest, [{form ++ [dot]}] ++ acc)
end

defp parse_forms(form_statements) do
try do
res =
Enum.map(
form_statements,
fn {form_statement} ->
case :erl_parse.parse_form(form_statement) do
{:ok, form} -> form
err -> throw({:parse_fail, err})
end
end
)

{:ok, res}
catch
{:parse_fail, err} -> err
end
end

# Create module - tokens from string
# Based on: https://stackoverflow.com/questions/2160660/how-to-compile-erlang-code-loaded-into-a-string
# The function will first assume that code starting with -module( is a erlang module definition

# Step 1: Scan the code
# Step 2: Convert to forms
# Step 3: Extract module name
# Step 4: Compile and load
defp eval_module(:erlang, code, binding, env) do
try do
with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(code), {1, 1}, [:text]),
{:ok, form_statements} <- tokens_to_forms(tokens),
{:ok, forms} <- parse_forms(form_statements) do
# First statement - form = module definition
{:attribute, _, :module, module_name} = hd(forms)

# Compile the forms from the code-block
{:ok, _, binary_module} = :compile.forms(forms)
:code.load_binary(module_name, ~c"nofile", binary_module)
{{:ok, ~c"erlang module successfully compiled", binding, env}, []}
else
# Tokenizer error - https://www.erlang.org/doc/man/erl_scan.html#string-3
{:error, {location, module, description}, _end_loc} ->
process_erlang_error(env, code, location, module, description)

# Parser error - https://www.erlang.org/doc/man/erl_parse.html#parse_form-1
{:error, {location, module, description}} ->
process_erlang_error(env, code, location, module, description)
end
catch
kind, error ->
stacktrace = prune_stacktrace(:erl_eval, __STACKTRACE__)
{{:error, kind, error, stacktrace}, []}
end
end

defp eval_statements(:erlang, code, binding, env) do
try do
erl_binding =
Enum.reduce(binding, %{}, fn {name, value}, erl_binding ->
Expand Down
13 changes: 13 additions & 0 deletions test/livebook/runtime/evaluator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,19 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert_receive {:runtime_evaluation_response, :code_1, {:text, "6"}, metadata()}
end

test "evaluate erlang-module code", %{evaluator: evaluator} do
Evaluator.evaluate_code(
evaluator,
:erlang,
"-module(tryme). -export([go/0]). go() ->{ok,went}.",
:code_4,
[]
)

assert_receive {:runtime_evaluation_response, :code_4,
{:text, "\"erlang module successfully compiled\""}, metadata()}
end

test "mixed erlang/elixir bindings", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, :elixir, "x = 1", :code_1, [])
Evaluator.evaluate_code(evaluator, :erlang, "Y = X.", :code_2, [:code_1])
Expand Down

0 comments on commit 1447859

Please sign in to comment.