Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Erlang error improvements #1960

Merged
merged 7 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 70 additions & 19 deletions lib/livebook/runtime/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ->
Expand All @@ -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()
Expand Down
13 changes: 13 additions & 0 deletions lib/livebook/runtime/evaluator/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
22 changes: 22 additions & 0 deletions test/livebook/runtime/evaluator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down