From 1447859bd1518d7c626ed72e87184f5ca326557b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20N=2EC=2E=20van=20=C2=B4t=20Hooft?= Date: Wed, 19 Jul 2023 14:00:47 -0300 Subject: [PATCH] Add erlang-module support 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 ) --- lib/livebook/runtime/evaluator.ex | 92 ++++++++++++++++++++++++ test/livebook/runtime/evaluator_test.exs | 13 ++++ 2 files changed, 105 insertions(+) diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index 7043eb5b7021..2baa9e0d96a6 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -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(¬_dot/1, tokens) + [dot | rest] = :lists.dropwhile(¬_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 -> diff --git a/test/livebook/runtime/evaluator_test.exs b/test/livebook/runtime/evaluator_test.exs index 3d0ac4ec04b4..767ce636b326 100644 --- a/test/livebook/runtime/evaluator_test.exs +++ b/test/livebook/runtime/evaluator_test.exs @@ -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])