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
@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.
The evaluator stores the resulting binding and environment under `ref`.
Any subsequent calls may specify `prev_ref` pointing to a previous 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()
def evaluate_code(evaluator, code, ref, prev_ref \\ :initial) when ref != :initial do
response = GenServer.call(evaluator, {:evaluate_code, code, ref, prev_ref}, :infinity)
if response == :invalid_prev_ref do
raise ArgumentError, message: "invalid reference to previous evaluation: #{prev_ref}"
end
response
@spec evaluate_code(t(), pid(), String.t(), ref(), ref()) :: :ok
def evaluate_code(evaluator, send_to, code, ref, prev_ref \\ :initial) when ref != :initial do
GenServer.cast(evaluator, {:evaluate_code, send_to, code, ref, prev_ref})
end
@doc """
@ -108,29 +104,26 @@ defmodule LiveBook.Evaluator do
end
@impl true
def handle_call({:evaluate_code, code, ref, prev_ref}, {from, _}, state) do
case Map.fetch(state.contexts, prev_ref) do
:error ->
{:reply, :invalid_prev_ref, state}
def handle_cast({:evaluate_code, send_to, code, ref, prev_ref}, state) do
Evaluator.IOProxy.configure(state.io_proxy, send_to, ref)
{:ok, context} ->
Evaluator.IOProxy.configure(state.io_proxy, from, ref)
context = Map.get(state.contexts, prev_ref, state.contexts.initial)
case eval(code, context.binding, context.env) do
{:ok, result, binding, env} ->
result_context = %{binding: binding, env: env}
new_contexts = Map.put(state.contexts, ref, result_context)
new_state = %{state | contexts: new_contexts}
case eval(code, context.binding, context.env) do
{:ok, result, binding, env} ->
result_context = %{binding: binding, env: env}
new_contexts = Map.put(state.contexts, ref, result_context)
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} ->
{:reply, {:error, kind, error, stacktrace}, state}
end
{:error, kind, error, stacktrace} ->
send(send_to, {:evaluator_response, {:error, kind, error, stacktrace}})
{:noreply, state}
end
end
@impl true
def handle_cast({:forget_evaluation, ref}, state) do
new_state = %{state | contexts: Map.delete(state.contexts, ref)}
{:noreply, new_state}

View file

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

View file

@ -16,44 +16,45 @@ defmodule LiveBook.EvaluatorTest do
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
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} =
result
assert_receive {:evaluator_response,
{:error, _kind, %CompileError{description: "undefined function x/0"},
_stacktrace}}
end
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
test "given invalid prev_ref raises an error", %{evaluator: evaluator} do
assert_raise ArgumentError, fn ->
Evaluator.evaluate_code(evaluator, ":ok", :code_1, :code_nonexistent)
end
test "given invalid prev_ref just uses default context", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, self(), ":hey", :code_1, :code_nonexistent)
assert_receive {:evaluator_response, {:ok, :hey}}
end
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
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
test "returns error along with its kind and stacktrace", %{evaluator: evaluator} do
@ -61,9 +62,10 @@ defmodule LiveBook.EvaluatorTest do
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
test "in case of an error returns only the relevant part of stacktrace", %{
@ -87,25 +89,28 @@ defmodule LiveBook.EvaluatorTest do
Cat.meow()
"""
result = Evaluator.evaluate_code(evaluator, code, :code_1)
Evaluator.evaluate_code(evaluator, self(), code, :code_1)
expected_stacktrace = [
{Math, :bad_math, 0, [file: 'nofile', line: 3]},
{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
describe "forget_evaluation/2" do
test "invalidates the given reference", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, "x = 1", :code_1)
Evaluator.forget_evaluation(evaluator, :code_1)
Evaluator.evaluate_code(evaluator, self(), "x = 1", :code_1)
assert_receive {:evaluator_response, _}
assert_raise ArgumentError, fn ->
Evaluator.evaluate_code(evaluator, ":ok", :code_2, :code_1)
end
Evaluator.forget_evaluation(evaluator, :code_1)
Evaluator.evaluate_code(evaluator, self(), "x", :code_2, :code_1)
assert_receive {:evaluator_response,
{:error, _kind, %CompileError{description: "undefined function x/0"},
_stacktrace}}
end
end
end