Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions repartee/lib/repartee/backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ defmodule Repartee.Backend do
end

@impl true
def eval(code, meta, state) do
def eval(code, meta, state), do: eval(code, meta, state, true)

# emit_all? says whether the expressions before the last one report their
# values too.
defp eval(code, meta, state, emit_all?) do
file = Map.get(meta, :file, "nrepl")
line = Map.get(meta, :line, 1)
column = Map.get(meta, :column, 1)
Expand All @@ -43,10 +47,7 @@ defmodule Repartee.Backend do

case Code.string_to_quoted(code, quoted_opts) do
{:ok, quoted} ->
{value, binding, env} =
Code.eval_quoted_with_env(quoted, state.binding, state.env, prune_binding: true)

{:ok, %{value: inspect(value, state.inspect_opts)}, %{state | binding: binding, env: env}}
eval_exprs(top_level(quoted), state, emit_all?)

{:error, {location, message, token}} ->
{:error, syntax_error(file, location, message, token), state}
Expand All @@ -56,7 +57,8 @@ defmodule Repartee.Backend do
@impl true
def load_file(contents, meta, state) do
file = Map.get(meta, :path, Map.get(meta, :name, "nrepl-load-file"))
eval(contents, %{file: file, line: 1}, state)
# Like nREPL, load-file reports just the last expression's value.
eval(contents, %{file: file, line: 1}, state, false)
end

@impl true
Expand Down Expand Up @@ -97,6 +99,38 @@ defmodule Repartee.Backend do
}
end

# Several top-level expressions parse into a block without metadata. A
# parenthesized block has some, and stays a single expression.
defp top_level({:__block__, [], [_ | _] = exprs}), do: exprs
defp top_level(quoted), do: [quoted]

# One expression at a time, like IEx, so each one gets a value. Values
# before the last go to the client right away.
defp eval_exprs([quoted], state, _emit_all?) do
{value, state} = eval_expr(quoted, state)
{:ok, %{value: inspect(value, state.inspect_opts)}, state}
end

defp eval_exprs([quoted | rest], state, emit_all?) do
{value, state} = eval_expr(quoted, state)
if emit_all?, do: :dialtone_backend.emit_value(inspect(value, state.inspect_opts))

# A later expression that fails keeps what this one did, as the
# client may already have its value.
try do
eval_exprs(rest, state, emit_all?)
catch
kind, reason -> {:error, format_exception(kind, reason, __STACKTRACE__, state), state}
end
end

defp eval_expr(quoted, state) do
# :prune_binding would keep only the variables this code reads or
# writes, and the session would lose the rest of them.
{value, binding, env} = Code.eval_quoted_with_env(quoted, state.binding, state.env)
{value, %{state | binding: binding, env: env}}
end

defp syntax_error(file, location, message, token) do
line = Keyword.get(List.wrap(location), :line, location)

Expand Down
7 changes: 7 additions & 0 deletions repartee/test/repartee_backend_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ defmodule Repartee.BackendTest do
assert value == "42"
end

test "variables survive evals that don't use them" do
{_, state} = eval!("x = 41", init_state())
{_, state} = eval!("y = 1", state)
{value, _} = eval!("x + y", state)
assert value == "42"
end

test "aliases persist across evals" do
{_, state} = eval!("alias String, as: S", init_state())
{value, _} = eval!(~s|S.upcase("neat")|, state)
Expand Down
38 changes: 33 additions & 5 deletions repartee/test/repartee_integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Repartee.IntegrationTest do
use ExUnit.Case

import Repartee.NreplClient,
only: [value_of: 1, out_of: 1, statuses_of: 1]
only: [value_of: 1, values_of: 1, out_of: 1, statuses_of: 1]

alias Repartee.NreplClient, as: Client

Expand Down Expand Up @@ -44,20 +44,48 @@ defmodule Repartee.IntegrationTest do
assert value_of(msgs) == "43"
end

test "each top-level expression gets a value", %{client: client} do
{session, client} = clone(client)
{msgs, client} = eval(client, session, "x = 1\nx + 1")
assert values_of(msgs) == ["1", "2"]
{msgs, _} = eval(client, session, "(x; x + 2)")
assert values_of(msgs) == ["3"]
end

test "a failing expression keeps what the ones before it did", %{client: client} do
{session, client} = clone(client)
{msgs, client} = eval(client, session, ~s|y = 1\nraise "boom"|)
assert values_of(msgs) == ["1"]
assert "eval-error" in statuses_of(msgs)
{msgs, _} = eval(client, session, "y")
assert value_of(msgs) == "1"
end

test "load-file sends only the last value", %{client: client} do
{msgs, _} =
Client.request(client, %{
"op" => "load-file",
"file" => "a = 2\na * 21",
"file-name" => "x.exs"
})

assert values_of(msgs) == ["42"]
end

test "captures stderr when asked to", %{client: client} do
:ok = :dialtone.capture_stderr()
on_exit(fn -> Supervisor.terminate_child(:dialtone_sup, :dialtone_stderr) end)
{session, client} = clone(client)
{msgs, _} = eval(client, session, ~s|IO.write(:stderr, "oops")\n:ok|)
{msgs, _} = eval(client, session, ~s|IO.write(:stderr, "oops")\n:done|)
assert Enum.map_join(msgs, &Map.get(&1, "err", "")) == "oops"
assert value_of(msgs) == ":ok"
assert values_of(msgs) == [":ok", ":done"]
end

test "streams stdout", %{client: client} do
{session, client} = clone(client)
{msgs, _} = eval(client, session, ~s|IO.puts("здравей")\n:ok|)
{msgs, _} = eval(client, session, ~s|IO.puts("здравей")\n:done|)
assert out_of(msgs) == "здравей\n"
assert value_of(msgs) == ":ok"
assert values_of(msgs) == [":ok", ":done"]
end

test "stdin roundtrip via IO.gets", %{client: client} do
Expand Down
4 changes: 3 additions & 1 deletion repartee/test/support/nrepl_client.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ defmodule Repartee.NreplClient do
end

def value_of(msgs) do
[value] = for %{"value" => v} <- msgs, do: v
[value] = values_of(msgs)
value
end

def values_of(msgs), do: for(%{"value" => v} <- msgs, do: v)

def out_of(msgs) do
Enum.map_join(for(%{"out" => o} <- msgs, do: o), "", & &1)
end
Expand Down
Loading