diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index 82563c79c..e4eb16a1e 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -683,7 +683,7 @@ defmodule Livebook.Runtime.Evaluator do :erl_eval.add_binding(elixir_to_erlang_var(name), value, erl_binding) end) - with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(code)), + with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(code), {1, 1}, [:text]), {: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 @@ -720,26 +720,12 @@ defmodule Livebook.Runtime.Evaluator do {{:ok, result, binding, env}, []} else # Tokenizer error - {:error, err, location} -> - code_marker = %{ - line: :erl_anno.line(location), - severity: :error, - description: "Tokenizer #{err}" - } - - {{:error, :error, {:token, err}, []}, filter_erlang_code_markers([code_marker])} + {:error, {location, module, description}, _end_loc} -> + process_erlang_error(env, code, location, module, description) # Parser error - {:error, {location, _module, err}} -> - err = :erlang.list_to_binary(err) - - code_marker = %{ - line: :erl_anno.line(location), - severity: :error, - description: "Parser #{err}" - } - - {{:error, :error, err, []}, filter_erlang_code_markers([code_marker])} + {:error, {location, module, description}} -> + process_erlang_error(env, code, location, module, description) end catch kind, error -> @@ -748,6 +734,71 @@ defmodule Livebook.Runtime.Evaluator do end end + defp process_erlang_error(env, code, location, module, description) do + line = :erl_anno.line(location) + + formatted = + module.format_error(description) + |> :erlang.list_to_binary() + + code_marker = %{ + line: line, + severity: :error, + description: "#{module}: #{formatted}" + } + + error_cons = + case {module, description} do + {:erl_parse, [~c"syntax error before: ", []]} -> + &TokenMissingError.exception/1 + + _ -> + &SyntaxError.exception/1 + end + + error = + error_cons.( + file: env.file, + line: line, + column: + case :erl_anno.column(location) do + :undefined -> 1 + val -> val + end, + description: formatted, + snippet: make_snippet(code, location) + ) + + {{:error, :error, error, []}, filter_erlang_code_markers([code_marker])} + end + + defp make_snippet(code, location) do + start_line = 1 + start_column = 1 + line = :erl_anno.line(location) + + case :erl_anno.column(location) do + :undefined -> + nil + + column -> + lines = :string.split(code, "\n", :all) + snippet = :lists.nth(line - start_line + 1, lines) + + offset = + if line == start_line do + column - start_column + else + column - 1 + end + + case :string.trim(code, :leading) do + [] -> nil + _ -> %{content: snippet, offset: offset} + end + end + end + defp elixir_to_erlang_var(name) do name |> :erlang.atom_to_binary() diff --git a/lib/livebook/runtime/evaluator/formatter.ex b/lib/livebook/runtime/evaluator/formatter.ex index b47534fa4..aa287778e 100644 --- a/lib/livebook/runtime/evaluator/formatter.ex +++ b/lib/livebook/runtime/evaluator/formatter.ex @@ -31,6 +31,19 @@ defmodule Livebook.Runtime.Evaluator.Formatter do to_output(value) end + def format_result({:error, kind, error, stacktrace}, :erlang) do + if is_exception(error) do + format_result({:error, kind, error, stacktrace}, :elixir) + else + formatted = + :erl_error.format_exception(kind, error, stacktrace) + |> error_color + |> :erlang.list_to_binary() + + {:error, formatted, error_type(error)} + end + end + def format_result({:error, kind, error, stacktrace}, _language) do formatted = format_error(kind, error, stacktrace) {:error, formatted, error_type(error)} diff --git a/test/livebook/runtime/evaluator_test.exs b/test/livebook/runtime/evaluator_test.exs index 4fa8313da..c6efe71b5 100644 --- a/test/livebook/runtime/evaluator_test.exs +++ b/test/livebook/runtime/evaluator_test.exs @@ -1168,6 +1168,28 @@ defmodule Livebook.Runtime.EvaluatorTest do assert metadata.code_markers == [] end + + test "syntax and tokenizer errors are converted", %{evaluator: evaluator} do + # Incomplete input + Evaluator.evaluate_code(evaluator, :erlang, "X =", :code_1, []) + assert_receive {:runtime_evaluation_response, :code_1, {:error, message, _}, metadata()} + assert "\e[31m** (TokenMissingError)" <> _ = message + + # Parser error + Evaluator.evaluate_code(evaluator, :erlang, "X ==/== a.", :code_2, []) + assert_receive {:runtime_evaluation_response, :code_2, {:error, message, _}, metadata()} + assert "\e[31m** (SyntaxError)" <> _ = message + + # Tokenizer error + Evaluator.evaluate_code(evaluator, :erlang, "$a$", :code_3, []) + assert_receive {:runtime_evaluation_response, :code_3, {:error, message, _}, metadata()} + assert "\e[31m** (SyntaxError)" <> _ = message + + # Erlang exception + Evaluator.evaluate_code(evaluator, :erlang, "list_to_binary(1).", :code_4, []) + assert_receive {:runtime_evaluation_response, :code_4, {:error, message, _}, metadata()} + assert "\e[31mexception error: bad argument" <> _ = message + end end describe "formatting" do