diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index 2343b8479f6..59193fcefbf 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -434,7 +434,9 @@ defmodule Livebook.Runtime.Evaluator do end start_time = System.monotonic_time() + {eval_result, code_markers} = eval(language, code, context.binding, context.env) + evaluation_time_ms = time_diff_ms(start_time) %{tracer_info: tracer_info} = Evaluator.IOProxy.after_evaluation(state.io_proxy) @@ -689,19 +691,127 @@ defmodule Livebook.Runtime.Evaluator do end defp eval(:erlang, code, binding, env) do + case :erl_scan.string(String.to_charlist(code), {1, 1}, [:text]) do + {:ok, tokens, _} -> + case find_first_module_attribute(tokens) do + {:ok, _module_name} -> + eval_erlang_module(code, tokens, binding, env) + + :error -> + eval_erlang_statements(code, tokens, binding, env) + end + + # Tokenizer error + {:error, {location, module, description}, _end_loc} -> + process_erlang_error(env, code, location, module, description) + end + end + + # ------------------------------------------------------------------------ + # Simple Erlang Module - helper functions + # ------------------------------------------------------------------------ + # Start of an attribute + defp is_not_module_declaration({:-, _}), do: false + # Ignore comments + defp is_not_module_declaration({_, _, :comment, _}), do: true + # Ignore everything else + defp is_not_module_declaration(_), do: true + + # Find the first valid module declaration in a token list + defp find_first_module_attribute(tokens) do + case :lists.dropwhile(&is_not_module_declaration/1, tokens) do + # Multiple declarations of a module could be defined, that's fine, we need at least one to run in module mode + [ + {:-, _}, + {:atom, _, :module}, + {:"(", _}, + {:atom, _, module_name}, + {:")", _}, + {:dot, _} + | _ + ] -> + {:ok, module_name} + + _ -> + # No -module(module_name). attribute + :error + end + end + + defp tokens_to_forms([], acc) do + {:ok, :lists.reverse(acc)} + end + + defp tokens_to_forms(tokens, acc) do + {form, rest} = until_dot(tokens, []) + + case :erl_parse.parse_form(form) do + {:ok, form} -> tokens_to_forms(rest, [form | acc]) + {:error, reason} -> {:error, reason} + end + end + + defp until_dot([{:dot, _} = head | tail], acc), + do: {Enum.reverse([head | acc]), tail} + + defp until_dot([head | tail], acc), + do: until_dot(tail, [head | acc]) + + defp until_dot([], acc), + do: {Enum.reverse(acc), []} + + # Create module - tokens from string + # Based on: https://stackoverflow.com/questions/2160660/how-to-compile-erlang-code-loaded-into-a-string + # Step 1: Scan the code + # Step 2: Convert to forms + # Step 3: Extract module name + # Step 4: Compile and load + # Step 5: If compile success - register module + + defp eval_erlang_module(_code, tokens, binding, env) do + try do + {:ok, forms} = tokens_to_forms(tokens, []) + + case :compile.forms(forms) do + {:ok, _, binary_module} -> + # First statement - form = module definition + {:attribute, _, :module, module} = hd(forms) + # Line below should be done in Tracer... + {:module, module} = + :code.load_binary(module, ~c"#{module}.beam", binary_module) + + # Registration of module + env = %{env | module: module, versioned_vars: %{}} + Evaluator.Tracer.trace({:on_module, binary_module, %{}}, env) + result = {:ok, module} + {{:ok, result, binding, env}, []} + + :error -> + error = "compile.forms failed - syntax error" + {{:error, :error, error, []}, []} + # process_erlang_error(env, code, {1,1}, :compile,"Compile forms failed") + end + catch + kind, error -> + stacktrace = prune_stacktrace(:erl_eval, __STACKTRACE__) + {{:error, kind, error, stacktrace}, []} + end + end + + defp eval_erlang_statements(code, tokens, binding, env) do try do erl_binding = Enum.reduce(binding, %{}, fn {name, value}, erl_binding -> :erl_eval.add_binding(elixir_to_erlang_var(name), value, erl_binding) end) - with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(code), {1, 1}, [:text]), - {:ok, parsed} <- :erl_parse.parse_exprs(tokens), + with {:ok, parsed} <- :erl_parse.parse_exprs(tokens), {:value, result, new_erl_binding} <- :erl_eval.exprs(parsed, erl_binding) do # Simple heuristic to detect the used variables. We look at # the tokens and assume all var tokens are used variables. # This will not handle shadowing of variables in fun definitions # and will only work well enough for expressions, not for modules. + used_vars = for {:var, _anno, name} <- tokens, do: {erlang_to_elixir_var(name), nil}, @@ -731,10 +841,6 @@ defmodule Livebook.Runtime.Evaluator do {{:ok, result, binding, env}, []} else - # Tokenizer error - {:error, {location, module, description}, _end_loc} -> - process_erlang_error(env, code, location, module, description) - # Parser error {:error, {location, module, description}} -> process_erlang_error(env, code, location, module, description) @@ -873,11 +979,12 @@ defmodule Livebook.Runtime.Evaluator do do: {:module, module}, into: identifiers_used + # Note: `module_info` works for both Erlang and Elixir modules, as opposed to `__info__` + # TODO: Find out why we need the case - identifiers_defined = - for {module, _line_vars} <- tracer_info.modules_defined, - version = module.__info__(:md5), - do: {{:module, module}, version}, - into: identifiers_defined + for {module, _line_vars} <- tracer_info.modules_defined, into: identifiers_defined do + {{:module, module}, module.module_info(:md5)} + end # Aliases diff --git a/test/livebook/runtime/evaluator_test.exs b/test/livebook/runtime/evaluator_test.exs index 6d2fe89f079..571628aee07 100644 --- a/test/livebook/runtime/evaluator_test.exs +++ b/test/livebook/runtime/evaluator_test.exs @@ -1300,6 +1300,74 @@ defmodule Livebook.Runtime.EvaluatorTest do assert_receive {:runtime_evaluation_response, :code_1, terminal_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, terminal_text(_), metadata()} + end + + test "evaluate erlang-module error function already defined", %{evaluator: evaluator} do + Evaluator.evaluate_code( + evaluator, + :erlang, + "-module(tryme). -export([go/0]). go() ->{ok,went}. go() ->{ok,went}.", + :code_4, + [] + ) + + assert_receive { + :runtime_evaluation_response, + :code_4, + error(message), + metadata() + } + assert message =~ "compile.forms failed - syntax error" + end + + test "evaluate erlang-module error - expression after module", %{evaluator: evaluator} do + Evaluator.evaluate_code( + evaluator, + :erlang, + "-module(tryme). -export([go/0]). go() ->{ok,went}. go() ->{ok,went}. A = 1.", + :code_4, + [] + ) + + assert_receive { + :runtime_evaluation_response, + :code_4, + error(message), + metadata() + } + + assert message =~ "\"syntax error before: \",\"A\"" + end + + test "evaluate erlang-module error - two modules", %{evaluator: evaluator} do + Evaluator.evaluate_code( + evaluator, + :erlang, + "-module(one). -export([go/0]). go() ->{ok,one}. -module(two). -export([go/0]). go() ->{ok,two}.", + :code_4, + [] + ) + + assert_receive { + :runtime_evaluation_response, + :code_4, + error(message), + metadata() + } + + assert message =~ "compile.forms failed - syntax error" + 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])