- 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>
111 lines
3.1 KiB
Elixir
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
|