diff --git a/lib/live_book/evaluator.ex b/lib/live_book/evaluator.ex index c07b90d47..9b4a1a95f 100644 --- a/lib/live_book/evaluator.ex +++ b/lib/live_book/evaluator.ex @@ -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} diff --git a/lib/live_book/session.ex b/lib/live_book/session.ex index 0dea6ad7a..7b608c011 100644 --- a/lib/live_book/session.ex +++ b/lib/live_book/session.ex @@ -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 diff --git a/test/live_book/evaluator_test.exs b/test/live_book/evaluator_test.exs index bcd4ad712..456cf62f8 100644 --- a/test/live_book/evaluator_test.exs +++ b/test/live_book/evaluator_test.exs @@ -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