diff --git a/repartee/lib/repartee/backend.ex b/repartee/lib/repartee/backend.ex index 0213e76..69c6830 100644 --- a/repartee/lib/repartee/backend.ex +++ b/repartee/lib/repartee/backend.ex @@ -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) @@ -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} @@ -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 @@ -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) diff --git a/repartee/test/repartee_backend_test.exs b/repartee/test/repartee_backend_test.exs index 3d5c1cc..89c1322 100644 --- a/repartee/test/repartee_backend_test.exs +++ b/repartee/test/repartee_backend_test.exs @@ -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) diff --git a/repartee/test/repartee_integration_test.exs b/repartee/test/repartee_integration_test.exs index c10c590..6568103 100644 --- a/repartee/test/repartee_integration_test.exs +++ b/repartee/test/repartee_integration_test.exs @@ -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 @@ -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 diff --git a/repartee/test/support/nrepl_client.exs b/repartee/test/support/nrepl_client.exs index eb3c62d..1182cab 100644 --- a/repartee/test/support/nrepl_client.exs +++ b/repartee/test/support/nrepl_client.exs @@ -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