Packages
livebook
0.1.0
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/evaluator.ex
defmodule Livebook.Evaluator do
@moduledoc false
# A process responsible for evaluating notebook code.
#
# The process receives evaluation request and synchronously
# evaluates the given code within itself (rather than spawning a separate process).
# It stores the resulting binding and env as part of the state.
#
# It's important to store the binding in the same process
# where the evaluation happens, as otherwise we would have to
# send them between processes, effectively copying potentially large data.
use GenServer, restart: :temporary
alias Livebook.Evaluator
@type t :: GenServer.server()
@type state :: %{
io_proxy: pid(),
contexts: %{ref() => context()}
}
@typedoc """
An evaluation context.
"""
@type context :: %{binding: Code.binding(), env: Macro.Env.t()}
@typedoc """
A term used to identify evaluation.
"""
@type ref :: term()
@typedoc """
Either {:ok, result} for successful evaluation
or {:error, kind, error, stacktrace} for a failed one.
"""
@type evaluation_response ::
{:ok, any()} | {:error, Exception.kind(), any(), Exception.stacktrace()}
## API
@doc """
Starts the evaluator.
Options:
* `formatter` - a module implementing the `Livebook.Evaluator.Formatter` behaviour,
used for transforming evaluation response before it's sent to the client
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts)
end
@doc """
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, ref, response}`.
Note that response is transformed with the configured formatter (identity by default).
## Options
* `:file` - file to which the evaluated code belongs. Most importantly,
this has an impact on the value of `__DIR__`.
"""
@spec evaluate_code(t(), pid(), String.t(), ref(), ref() | nil, keyword()) :: :ok
def evaluate_code(evaluator, send_to, code, ref, prev_ref \\ nil, opts \\ []) when ref != nil do
GenServer.cast(evaluator, {:evaluate_code, send_to, code, ref, prev_ref, opts})
end
@doc """
Removes the evaluation identified by `ref` from history,
so that further evaluations cannot use it.
"""
@spec forget_evaluation(t(), ref()) :: :ok
def forget_evaluation(evaluator, ref) do
GenServer.cast(evaluator, {:forget_evaluation, ref})
end
@doc """
Asynchronously finds completion items matching the given `hint` text.
If `evaluation_ref` is given, its binding and environment are also
used for the completion. Response is sent to the `send_to` process
as `{:completion_response, ref, items}`.
"""
@spec request_completion_items(t(), pid(), term(), String.t(), ref() | nil) :: :ok
def request_completion_items(evaluator, send_to, ref, hint, evaluation_ref \\ nil) do
GenServer.cast(evaluator, {:request_completion_items, send_to, ref, hint, evaluation_ref})
end
## Callbacks
@impl true
def init(opts) do
formatter = Keyword.get(opts, :formatter, Evaluator.IdentityFormatter)
{:ok, io_proxy} = Evaluator.IOProxy.start_link()
# Use the dedicated IO device as the group leader,
# so that it handles all :stdio operations.
Process.group_leader(self(), io_proxy)
{:ok, initial_state(formatter, io_proxy)}
end
defp initial_state(formatter, io_proxy) do
%{
formatter: formatter,
io_proxy: io_proxy,
contexts: %{}
}
end
defp initial_context() do
env = :elixir.env_for_eval([])
%{binding: [], env: env}
end
@impl true
def handle_cast({:evaluate_code, send_to, code, ref, prev_ref, opts}, state) do
Evaluator.IOProxy.configure(state.io_proxy, send_to, ref)
context = Map.get_lazy(state.contexts, prev_ref, fn -> initial_context() end)
file = Keyword.get(opts, :file, "nofile")
context = put_in(context.env.file, file)
{result_context, response} =
case eval(code, context.binding, context.env) do
{:ok, result, binding, env} ->
result_context = %{binding: binding, env: env}
response = {:ok, result}
{result_context, response}
{:error, kind, error, stacktrace} ->
response = {:error, kind, error, stacktrace}
{context, response}
end
Evaluator.IOProxy.flush(state.io_proxy)
send_evaluation_response(send_to, ref, response, state.formatter)
new_state = put_in(state.contexts[ref], result_context)
{:noreply, new_state}
end
def handle_cast({:forget_evaluation, ref}, state) do
new_state = %{state | contexts: Map.delete(state.contexts, ref)}
{:noreply, new_state}
end
def handle_cast({:request_completion_items, send_to, ref, hint, evaluation_ref}, state) do
context = Map.get_lazy(state.contexts, evaluation_ref, fn -> initial_context() end)
items = Livebook.Completion.get_completion_items(hint, context.binding, context.env)
send(send_to, {:completion_response, ref, items})
{:noreply, state}
end
defp send_evaluation_response(send_to, ref, evaluation_response, formatter) do
response = formatter.format(evaluation_response)
send(send_to, {:evaluation_response, ref, response})
end
defp eval(code, binding, env) do
try do
quoted = Code.string_to_quoted!(code)
{result, binding, env} = :elixir.eval_quoted(quoted, binding, env)
{:ok, result, binding, env}
catch
kind, error ->
{kind, error, stacktrace} = prepare_error(kind, error, __STACKTRACE__)
{:error, kind, error, stacktrace}
end
end
defp prepare_error(kind, error, stacktrace) do
{error, stacktrace} = Exception.blame(kind, error, stacktrace)
stacktrace = prune_stacktrace(stacktrace)
{kind, error, stacktrace}
end
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
@elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_pass]
defp prune_stacktrace(stacktrace) do
# The order in which each drop_while is listed is important.
# For example, the user may call Code.eval_string/2 in their code
# and if there is an error we should not remove erl_eval
# and eval_bits information from the user stacktrace.
stacktrace
|> Enum.reverse()
|> Enum.drop_while(&(elem(&1, 0) == :proc_lib))
|> Enum.drop_while(&(elem(&1, 0) == :gen_server))
|> Enum.drop_while(&(elem(&1, 0) == __MODULE__))
|> Enum.drop_while(&(elem(&1, 0) == :elixir))
|> Enum.drop_while(&(elem(&1, 0) in [:erl_eval, :eval_bits]))
|> Enum.reverse()
|> Enum.reject(&(elem(&1, 0) in @elixir_internals))
end
end