Files
CljElixir/lib/clj_elixir/repl.ex
Adam 7e82efd7ec 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>
2026-03-08 11:03:10 -04:00

111 lines
3.1 KiB
Elixir

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