Files
CljElixir/lib/mix/tasks/clje.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

137 lines
3.0 KiB
Elixir

defmodule Mix.Tasks.Clje.Repl do
@moduledoc """
Starts an interactive CljElixir REPL.
## Usage
mix clje.repl
Features:
- Full line editing (arrow keys, home/end, backspace)
- Multi-line input (auto-detects unbalanced parens)
- `pr-str` output formatting
- Bindings persist across evaluations
- Special commands: :quit, :history, :help
For readline-style history (up/down arrow recall), run with rlwrap:
rlwrap mix clje.repl
"""
use Mix.Task
@shortdoc "Start an interactive CljElixir REPL"
@impl Mix.Task
def run(_args) do
# Ensure the project is compiled first
Mix.Task.run("compile")
# Start the application
Mix.Task.run("app.start")
IO.puts("CljElixir REPL v0.1.0")
IO.puts("Type :help for help, :quit to exit\n")
state = CljElixir.REPL.new()
loop(state)
end
defp loop(state) do
prompt = "clje:#{state.counter}> "
case read_input(prompt) do
:eof ->
IO.puts("\nBye!")
input ->
input = String.trim(input)
case input do
":quit" ->
IO.puts("Bye!")
":history" ->
print_history(state)
loop(state)
":help" ->
print_help()
loop(state)
"" ->
loop(state)
_ ->
# Check for multi-line - read more if unbalanced
full_input = maybe_read_more(input)
case CljElixir.REPL.eval(full_input, state) do
{:ok, result_str, new_state} ->
IO.puts(result_str)
loop(new_state)
{:error, error_str, new_state} ->
IO.puts("\e[31m#{error_str}\e[0m")
loop(new_state)
end
end
end
end
defp read_input(prompt) do
case IO.gets(prompt) do
:eof -> :eof
{:error, _} -> :eof
data -> data
end
end
defp maybe_read_more(input) do
if CljElixir.REPL.balanced?(input) do
input
else
read_continuation(input)
end
end
defp read_continuation(acc) do
case IO.gets(" ") do
:eof -> acc
{:error, _} -> acc
line ->
new_acc = acc <> "\n" <> String.trim_trailing(line, "\n")
if CljElixir.REPL.balanced?(new_acc) do
new_acc
else
read_continuation(new_acc)
end
end
end
defp print_history(state) do
state.history
|> Enum.reverse()
|> Enum.with_index(1)
|> Enum.each(fn {expr, i} ->
IO.puts(" #{i}: #{String.replace(expr, "\n", "\n ")}")
end)
end
defp print_help do
IO.puts("""
CljElixir REPL Commands:
:help - show this help
:history - show expression history
:quit - exit the REPL
Tips:
- Multi-line input: unbalanced parens trigger continuation
- Bindings persist: (def x 42) makes x available in later expressions
- All CljElixir syntax is supported
- For up/down arrow history, run: rlwrap mix clje.repl
""")
end
end