Phase 8: REPL, printing, source maps, and nREPL server
- IPrintWithWriter protocol + CljElixir.Printer module with pr-str, print-str, pr, prn for all BEAM types (EDN-like output) - Source-mapped error messages: line/col metadata from reader now propagated through transformer into Elixir AST for accurate error locations in .clje files - Interactive REPL (mix clje.repl) with multi-line input detection, history, bindings persistence, and pr-str formatted output - nREPL server (mix clje.nrepl) with TCP transport, Bencode wire protocol, session management, and core operations (clone, close, eval, describe, ls-sessions, load-file, interrupt, completions). Writes .nrepl-port for editor auto-discovery. 92 new tests (699 total, 0 failures). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
defmodule CljElixir.REPL do
|
||||
@moduledoc """
|
||||
CljElixir Read-Eval-Print Loop engine.
|
||||
|
||||
Maintains state across evaluations: bindings persist,
|
||||
modules defined in one evaluation are available in the next.
|
||||
"""
|
||||
|
||||
defstruct bindings: [],
|
||||
history: [],
|
||||
counter: 1,
|
||||
env: nil
|
||||
|
||||
@doc "Create a new REPL state"
|
||||
def new do
|
||||
%__MODULE__{}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluate a CljElixir source string in the given REPL state.
|
||||
Returns {:ok, result_string, new_state} or {:error, error_string, new_state}.
|
||||
"""
|
||||
def eval(source, state) do
|
||||
opts = [
|
||||
bindings: state.bindings,
|
||||
file: "repl"
|
||||
]
|
||||
|
||||
case CljElixir.Compiler.eval_string(source, opts) do
|
||||
{:ok, result, new_bindings} ->
|
||||
result_str = CljElixir.Printer.pr_str(result)
|
||||
|
||||
new_state = %{state |
|
||||
bindings: new_bindings,
|
||||
history: [source | state.history],
|
||||
counter: state.counter + 1
|
||||
}
|
||||
|
||||
{:ok, result_str, new_state}
|
||||
|
||||
{:error, errors} ->
|
||||
error_str = format_errors(errors)
|
||||
new_state = %{state | counter: state.counter + 1}
|
||||
{:error, error_str, new_state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Check if input has balanced delimiters (parens, brackets, braces)"
|
||||
def balanced?(input) do
|
||||
input
|
||||
|> String.graphemes()
|
||||
|> count_delimiters(0, 0, 0, false, false)
|
||||
end
|
||||
|
||||
# Count open/close delimiters, respecting strings and comments
|
||||
defp count_delimiters([], parens, brackets, braces, _in_string, _escape) do
|
||||
parens == 0 and brackets == 0 and braces == 0
|
||||
end
|
||||
|
||||
defp count_delimiters([char | rest], p, b, br, in_string, escape) do
|
||||
cond do
|
||||
escape ->
|
||||
# Previous char was \, skip this one
|
||||
count_delimiters(rest, p, b, br, in_string, false)
|
||||
|
||||
char == "\\" and in_string ->
|
||||
count_delimiters(rest, p, b, br, in_string, true)
|
||||
|
||||
char == "\"" and not in_string ->
|
||||
count_delimiters(rest, p, b, br, true, false)
|
||||
|
||||
char == "\"" and in_string ->
|
||||
count_delimiters(rest, p, b, br, false, false)
|
||||
|
||||
in_string ->
|
||||
count_delimiters(rest, p, b, br, true, false)
|
||||
|
||||
char == ";" ->
|
||||
# Comment - skip rest of line
|
||||
rest_after_newline = Enum.drop_while(rest, &(&1 != "\n"))
|
||||
count_delimiters(rest_after_newline, p, b, br, false, false)
|
||||
|
||||
char == "(" -> count_delimiters(rest, p + 1, b, br, false, false)
|
||||
char == ")" -> count_delimiters(rest, p - 1, b, br, false, false)
|
||||
char == "[" -> count_delimiters(rest, p, b + 1, br, false, false)
|
||||
char == "]" -> count_delimiters(rest, p, b - 1, br, false, false)
|
||||
char == "{" -> count_delimiters(rest, p, b, br + 1, false, false)
|
||||
char == "}" -> count_delimiters(rest, p, b, br - 1, false, false)
|
||||
|
||||
true -> count_delimiters(rest, p, b, br, false, false)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_errors(errors) when is_list(errors) do
|
||||
Enum.map_join(errors, "\n", fn
|
||||
%{message: msg, line: line} when is_integer(line) and line > 0 ->
|
||||
"Error on line #{line}: #{msg}"
|
||||
|
||||
%{message: msg} ->
|
||||
"Error: #{msg}"
|
||||
|
||||
other ->
|
||||
"Error: #{inspect(other)}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_errors(other) do
|
||||
"Error: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user