From 9296c481bb2061c53cb270b3c9e19f130ca39338 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. --- lib/livebook/runtime/evaluator.ex | 79 +++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index 7043eb5b7021..fdc59443f4bf 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -675,6 +675,85 @@ defmodule Livebook.Runtime.Evaluator do end defp eval(:erlang, code, binding, env) do + try 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 + catch + kind, error -> + stacktrace = prune_stacktrace(:erl_eval, __STACKTRACE__) + {{:error, kind, error, stacktrace}, []} + 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 + 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 + + # 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 + {:ok, tokens, _} = :erl_scan.string(String.to_charlist(code), {1, 1}, [:text]) + + form_statements = tokens_to_forms(tokens) + + forms = + Enum.map( + form_statements, + fn {form_statement} -> + {:ok, form} = :erl_parse.parse_form(form_statement) + form + end + ) + + # 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}, []} + 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 ->