mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-08 05:54:20 +08:00
Make code evaluation request async, so that we don't need an intermediary process
This commit is contained in:
parent
8d6d09899a
commit
d7e16563b6
3 changed files with 51 additions and 57 deletions
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue