Make code evaluation request async, so that we don't need an intermediary process

This commit is contained in:
Jonatan Kłosko 2021-01-12 00:57:07 +01:00
parent 8d6d09899a
commit d7e16563b6
3 changed files with 51 additions and 57 deletions

View file

@ -46,23 +46,19 @@ defmodule LiveBook.Evaluator do
end end
@doc """ @doc """
Synchronously parses and evaluates the given code. Asynchronously parses and evaluates the given code.
Any exceptions are captured, in which case this method returns an error. Any exceptions are captured, in which case this method returns an error.
The evaluator stores the resulting binding and environment under `ref`. The evaluator stores the resulting binding and environment under `ref`.
Any subsequent calls may specify `prev_ref` pointing to a previous evaluation, Any subsequent calls may specify `prev_ref` pointing to a previous evaluation,
in which case the corresponding binding and environment are used during evaluation. in which case the corresponding binding and environment are used during evaluation.
Evaluation response is sent to the process identified by `send_to` as `{:evaluation_response, response}`.
""" """
@spec evaluate_code(t(), String.t(), ref(), ref()) :: evaluation_response() @spec evaluate_code(t(), pid(), String.t(), ref(), ref()) :: :ok
def evaluate_code(evaluator, code, ref, prev_ref \\ :initial) when ref != :initial do def evaluate_code(evaluator, send_to, code, ref, prev_ref \\ :initial) when ref != :initial do
response = GenServer.call(evaluator, {:evaluate_code, code, ref, prev_ref}, :infinity) GenServer.cast(evaluator, {:evaluate_code, send_to, code, ref, prev_ref})
if response == :invalid_prev_ref do
raise ArgumentError, message: "invalid reference to previous evaluation: #{prev_ref}"
end
response
end end
@doc """ @doc """
@ -108,29 +104,26 @@ defmodule LiveBook.Evaluator do
end end
@impl true @impl true
def handle_call({:evaluate_code, code, ref, prev_ref}, {from, _}, state) do def handle_cast({:evaluate_code, send_to, code, ref, prev_ref}, state) do
case Map.fetch(state.contexts, prev_ref) do Evaluator.IOProxy.configure(state.io_proxy, send_to, ref)
:error ->
{:reply, :invalid_prev_ref, state}
{:ok, context} -> context = Map.get(state.contexts, prev_ref, state.contexts.initial)
Evaluator.IOProxy.configure(state.io_proxy, from, ref)
case eval(code, context.binding, context.env) do case eval(code, context.binding, context.env) do
{:ok, result, binding, env} -> {:ok, result, binding, env} ->
result_context = %{binding: binding, env: env} result_context = %{binding: binding, env: env}
new_contexts = Map.put(state.contexts, ref, result_context) new_contexts = Map.put(state.contexts, ref, result_context)
new_state = %{state | contexts: new_contexts} new_state = %{state | contexts: new_contexts}
{:reply, {:ok, result}, new_state} send(send_to, {:evaluator_response, {:ok, result}})
{:noreply, new_state}
{:error, kind, error, stacktrace} -> {:error, kind, error, stacktrace} ->
{:reply, {:error, kind, error, stacktrace}, state} send(send_to, {:evaluator_response, {:error, kind, error, stacktrace}})
end {:noreply, state}
end end
end end
@impl true
def handle_cast({:forget_evaluation, ref}, state) do def handle_cast({:forget_evaluation, ref}, state) do
new_state = %{state | contexts: Map.delete(state.contexts, ref)} new_state = %{state | contexts: Map.delete(state.contexts, ref)}
{:noreply, new_state} {:noreply, new_state}

View file

@ -233,7 +233,6 @@ defmodule LiveBook.Session do
{:ok, section} = Notebook.fetch_cell_section(notebook, cell_id) {:ok, section} = Notebook.fetch_cell_section(notebook, cell_id)
{state, evaluator} = get_section_evaluator(state, section.id) {state, evaluator} = get_section_evaluator(state, section.id)
%{source: source} = cell %{source: source} = cell
session_pid = self()
prev_ref = prev_ref =
case Notebook.parent_cells(notebook, cell_id) do case Notebook.parent_cells(notebook, cell_id) do
@ -241,10 +240,7 @@ defmodule LiveBook.Session do
[] -> :initial [] -> :initial
end end
spawn(fn -> Evaluator.evaluate_code(evaluator, self(), source, cell_id, prev_ref)
response = Evaluator.evaluate_code(evaluator, source, cell_id, prev_ref)
send(session_pid, {:evaluator_response, cell_id, response})
end)
state state
end end

View file

@ -16,44 +16,45 @@ defmodule LiveBook.EvaluatorTest do
x + y x + y
""" """
result = Evaluator.evaluate_code(evaluator, code, 1) Evaluator.evaluate_code(evaluator, self(), code, 1)
assert result == {:ok, 3} assert_receive {:evaluator_response, {:ok, 3}}
end end
test "given no prev_ref does not see previous evaluation context", %{evaluator: evaluator} do test "given no prev_ref does not see previous evaluation context", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, "x = 1", :code_1) Evaluator.evaluate_code(evaluator, self(), "x = 1", :code_1)
result = Evaluator.evaluate_code(evaluator, "x", :code_2) Evaluator.evaluate_code(evaluator, self(), "x", :code_2)
assert {:error, _kind, %CompileError{description: "undefined function x/0"}, _stacktrace} = assert_receive {:evaluator_response,
result {:error, _kind, %CompileError{description: "undefined function x/0"},
_stacktrace}}
end end
test "given prev_ref sees previous evaluation context", %{evaluator: evaluator} do test "given prev_ref sees previous evaluation context", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, "x = 1", :code_1) Evaluator.evaluate_code(evaluator, self(), "x = 1", :code_1)
result = Evaluator.evaluate_code(evaluator, "x", :code_2, :code_1) Evaluator.evaluate_code(evaluator, self(), "x", :code_2, :code_1)
assert result == {:ok, 1} assert_receive {:evaluator_response, {:ok, 1}}
end end
test "given invalid prev_ref raises an error", %{evaluator: evaluator} do test "given invalid prev_ref just uses default context", %{evaluator: evaluator} do
assert_raise ArgumentError, fn -> Evaluator.evaluate_code(evaluator, self(), ":hey", :code_1, :code_nonexistent)
Evaluator.evaluate_code(evaluator, ":ok", :code_1, :code_nonexistent)
end assert_receive {:evaluator_response, {:ok, :hey}}
end end
test "captures standard output and sends it to the caller", %{evaluator: evaluator} do test "captures standard output and sends it to the caller", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, ~s{IO.puts("hey")}, :code_1) Evaluator.evaluate_code(evaluator, self(), ~s{IO.puts("hey")}, :code_1)
assert_received {:evaluator_stdout, :code_1, "hey\n"} assert_receive {:evaluator_stdout, :code_1, "hey\n"}
end end
test "using standard input results in an immediate error", %{evaluator: evaluator} do test "using standard input results in an immediate error", %{evaluator: evaluator} do
result = Evaluator.evaluate_code(evaluator, ~s{IO.gets("> ")}, :code_1) Evaluator.evaluate_code(evaluator, self(), ~s{IO.gets("> ")}, :code_1)
assert result == {:ok, {:error, :enotsup}} assert_receive {:evaluator_response, {:ok, {:error, :enotsup}}}
end end
test "returns error along with its kind and stacktrace", %{evaluator: evaluator} do test "returns error along with its kind and stacktrace", %{evaluator: evaluator} do
@ -61,9 +62,10 @@ defmodule LiveBook.EvaluatorTest do
List.first(%{}) List.first(%{})
""" """
result = Evaluator.evaluate_code(evaluator, code, :code_1) Evaluator.evaluate_code(evaluator, self(), code, :code_1)
assert {:error, :error, %FunctionClauseError{}, [{List, :first, 1, _location}]} = result assert_receive {:evaluator_response,
{:error, :error, %FunctionClauseError{}, [{List, :first, 1, _location}]}}
end end
test "in case of an error returns only the relevant part of stacktrace", %{ test "in case of an error returns only the relevant part of stacktrace", %{
@ -87,25 +89,28 @@ defmodule LiveBook.EvaluatorTest do
Cat.meow() Cat.meow()
""" """
result = Evaluator.evaluate_code(evaluator, code, :code_1) Evaluator.evaluate_code(evaluator, self(), code, :code_1)
expected_stacktrace = [ expected_stacktrace = [
{Math, :bad_math, 0, [file: 'nofile', line: 3]}, {Math, :bad_math, 0, [file: 'nofile', line: 3]},
{Cat, :meow, 0, [file: 'nofile', line: 10]} {Cat, :meow, 0, [file: 'nofile', line: 10]}
] ]
assert {:error, _kind, _error, ^expected_stacktrace} = result assert_receive {:evaluator_response, {:error, _kind, _error, ^expected_stacktrace}}
end end
end end
describe "forget_evaluation/2" do describe "forget_evaluation/2" do
test "invalidates the given reference", %{evaluator: evaluator} do test "invalidates the given reference", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, "x = 1", :code_1) Evaluator.evaluate_code(evaluator, self(), "x = 1", :code_1)
Evaluator.forget_evaluation(evaluator, :code_1) assert_receive {:evaluator_response, _}
assert_raise ArgumentError, fn -> Evaluator.forget_evaluation(evaluator, :code_1)
Evaluator.evaluate_code(evaluator, ":ok", :code_2, :code_1) Evaluator.evaluate_code(evaluator, self(), "x", :code_2, :code_1)
end
assert_receive {:evaluator_response,
{:error, _kind, %CompileError{description: "undefined function x/0"},
_stacktrace}}
end end
end end
end end